diff --git a/lib/std/core/array.c3 b/lib/std/core/array.c3 index e932a7026..8f4ef1786 100644 --- a/lib/std/core/array.c3 +++ b/lib/std/core/array.c3 @@ -1,7 +1,5 @@ module std::core::array; - -import std::core::array::slice; -import std::collections::pair; +import std::collections::pair, std::io; <* Returns true if the array contains at least one element, else false @@ -121,6 +119,216 @@ macro concat(Allocator allocator, arr1, arr2) @nodiscard macro tconcat(arr1, arr2) @nodiscard => concat(tmem, arr1, arr2); +<* + Apply a reduction/folding operation to an iterable type. This walks along the input array + and applies an `#operation` to each value, returning it to the `identity` (or "accumulator") + base value. + + For example: + ```c3 + int[] my_slice = { 1, 8, 12 }; + int folded = array::@reduce(my_slice, 2, fn (i, e) => i * e); + assert(folded == (2 * 1 * 8 * 12)); + ``` + + Notice how the given `identity` value started the multiplication chain at 2. When enumerating + `my_slice`, each element is accumulated onto the `identity` value with each sequential iteration. + ``` + i = 2; // identity value + i *= 1; // my_slice[0] + i *= 8; // my_slice[1] + i *= 12; // my_slice[2] + ``` + + @param [in] array + @param identity + @param #operation : "The reduction/folding labmda function or function pointer to apply." + + @require @is_valid_list(array) : "Expected a valid list" + @require $defined($typefrom(@reduce_fn(array, identity)) $func = #operation) : "Invalid lambda or function pointer type" +*> +macro @reduce(array, identity, #operation) +{ + $typefrom(@reduce_fn(array, identity)) $func = #operation; + foreach (index, element : array) identity = $func(identity, element, index); + return identity; +} + +<* + Apply a summation operator (+) to an identity value across a span of array elements + and return the final accumulated result. + + @pure + + @param [in] array + @param identity_value : "The base accumulator value to use for the sum" + + @require @is_valid_list(array) : "Expected a valid list" + @require $defined(array[0] + array[0]) : "Array element type must implement the '+' operator" + @require $defined($typeof(array[0]) t = identity_value) : "The identity type must be assignable to the array element type" +*> +macro @sum(array, identity_value = 0) +{ + return @reduce(array, ($typeof(array[0]))identity_value, fn (acc, e, u) => acc + e); +} + +<* + Apply a product operator (*) to an identity value across a span of array elements + and return the final accumulated result. + + @pure + + @param [in] array + @param identity_value : "The base accumulator value to use for the product" + + @require @is_valid_list(array) : "Expected a valid list" + @require $defined(array[0] * array[0]) : "Array element type must implement the '*' operator" + @require $defined($typeof(array[0]) t = identity_value) : "The identity type must be assignable to the array element type" +*> +macro @product(array, identity_value = 1) +{ + return @reduce(array, ($typeof(array[0]))identity_value, fn (acc, e, u) => acc * e); +} + +<* + Applies a given predicate function to each element of an array and returns a new + array of `usz` values, each element representing an index within the original array + where the predicate returned `true`. + + The `.len` value of the returned array can also be used to quickly identify how many + input array elements matched the predicate. + + For example: + ```c3 + int[] arr = { 0, 20, 4, 30 }; + int[] matched_indices = array::@indices_of(mem, arr, fn (u, a) => a > 10); + ``` + + The `matched_indices` variable should contain a dynamically-allocated array of `[1, 3]`, + and thus its count indicates that 2 of the 4 elements matched the predicate condition. + + @param [&inout] allocator + @param [in] array + @param #predicate + + @require @is_valid_list(array) : "Expected a valid list" + @require $defined($typefrom(@predicate_fn(array)) p = #predicate) +*> +macro usz[] @indices_of(Allocator allocator, array, #predicate) +{ + usz[] results = allocator::new_array(allocator, usz, find_len(array)); + usz matches; + + $typefrom(@predicate_fn(array)) $predicate = #predicate; + foreach (index, element : array) + { + if ($predicate(element, index)) results[matches++] = index; + } + + return results[:matches]; +} + +<* + Array `@indices_of` using the temp allocator. + + @param [in] array + @param #predicate + + @require @is_valid_list(array) : "Expected a valid list" + @require $defined($typefrom(@predicate_fn(array)) p = #predicate) +*> +macro usz[] @tindices_of(array, #predicate) +{ + return @indices_of(tmem, array, #predicate); +} + + +<* + Applies a predicate function to each element of an input array and returns a new array + containing shallow copies of _only_ the elements for which the predicate function returned + a `true` value. + + For example: + ```c3 + int[] my_arr = { 1, 2, 4, 10, 11, 45 }; + int[] evens = array::@filter(mem, my_arr, fn (e, u) => !(e % 2)); + assert(evens == (int[]){2, 4, 10 }); + ``` + + @param [&inout] allocator + @param [in] array + @param #predicate + + @require @is_valid_list(array) : "Expected a valid list" + @require $defined($typefrom(@predicate_fn(array)) p = #predicate) +*> +macro @filter(Allocator allocator, array, #predicate) @nodiscard +{ + var $InnerType = $typeof(array[0]); + + usz[] matched_indices = @indices_of(allocator, array, #predicate); + defer allocator::free(allocator, matched_indices.ptr); // can free this upon leaving this call + + if (!matched_indices.len) return ($InnerType[]){}; + + $InnerType[] result = allocator::new_array(allocator, $InnerType, matched_indices.len); + + foreach (i, index : matched_indices) result[i] = array[index]; + + return result; +} + +<* + Array `@filter` using the temp allocator. + + @param [in] array + @param #predicate + + @require @is_valid_list(array) : "Expected a valid list" + @require $defined($typefrom(@predicate_fn(array)) p = #predicate) +*> +macro @tfilter(array, #predicate) @nodiscard +{ + return @filter(tmem, array, #predicate); +} + + +<* + Returns `true` if _any_ element of the input array returns `true` when + the `#predicate` function is applied. + + @param [in] array + @param #predicate + + @require @is_valid_list(array) : "Expected a valid list" + @require $defined($typefrom(@predicate_fn(array)) p = #predicate) +*> +macro bool @any(array, #predicate) +{ + $typefrom(@predicate_fn(array)) $predicate = #predicate; + foreach (index, element : array) if ($predicate(element, index)) return true; + return false; +} + + +<* + Returns `true` if _all_ elements of the input array return `true` when + the `#predicate` function is applied. + + @param [in] array + @param #predicate + + @require @is_valid_list(array) : "Expected a valid list" + @require $defined($typefrom(@predicate_fn(array)) p = #predicate) +*> +macro bool @all(array, #predicate) +{ + $typefrom(@predicate_fn(array)) $predicate = #predicate; + foreach (index, element : array) if (!$predicate(element, index)) return false; + return true; +} + + <* Zip together two separate arrays/slices into a single array of Pairs or return values. Values will be collected up to the length of the shorter array if `fill_with` is left undefined; otherwise, they @@ -192,7 +400,6 @@ macro @zip(Allocator allocator, left, right, #operation = EMPTY_MACRO_SLOT, fill $Type = $typeof(#operation).returns; $endif - usz left_len = find_len(left); usz right_len = find_len(right); @@ -301,12 +508,25 @@ macro @zip_into(left, right, #operation) // --- helper functions +module std::core::array @private; -macro typeid @zip_into_fn(#left, #right) @private @const + +macro typeid @predicate_fn(#array) @const +{ + return $typeof(fn bool ($typeof(#array[0]) a, usz index = 0) => true).typeid; +} + +macro typeid @reduce_fn(#array, #identity) @const +{ + return @typeid(fn $typeof(#identity) ($typeof(#identity) i, $typeof(#array[0]) a, usz index = 0) => i); +} + +macro typeid @zip_into_fn(#left, #right) @const { return @typeid(fn $typeof(#left[0]) ($typeof(#left[0]) l, $typeof(#right[0]) r) => l); } -macro bool @is_valid_operation(#operation, #left, #right) @const @private + +macro bool @is_valid_operation(#operation, #left, #right) @const { $switch: $case @is_empty_macro_slot(#operation): @@ -318,12 +538,12 @@ macro bool @is_valid_operation(#operation, #left, #right) @const @private $endswitch } -macro bool @is_valid_list(#expr) @const @private +macro bool @is_valid_list(#expr) @const { return $defined(#expr[0]) &&& ($defined(#expr.len) ||| $defined(#expr.len())); } -macro bool @is_valid_fill(left, right, fill_with) @private +macro bool @is_valid_fill(left, right, fill_with) { if (@is_empty_macro_slot(fill_with)) return true; usz left_len = @select($defined(left.len()), left.len(), left.len); @@ -339,4 +559,4 @@ macro usz find_len(list) $else return list.len; $endif -} \ No newline at end of file +} diff --git a/releasenotes.md b/releasenotes.md index b8eb2f9e4..636be431a 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -83,6 +83,7 @@ - Deprecated `PollSubscribes` and `PollEvents` in favour of `PollSubscribe` and `PollEvent` and made them const enums. - Added `AsciiCharset` for matching ascii characters quickly. - Added `String.trim_charset`. +- Added array `@reduce`, `@filter`, `@any`, `@all`, `@sum`, `@product`, and `@indices_of` macros. ## 0.7.4 Change list diff --git a/src/compiler/sema_decls.c b/src/compiler/sema_decls.c index ea46763eb..beddfaa1c 100755 --- a/src/compiler/sema_decls.c +++ b/src/compiler/sema_decls.c @@ -4571,6 +4571,7 @@ bool sema_analyse_var_decl_ct(SemaContext *context, Decl *decl, bool *check_fail default: UNREACHABLE } + if (check_failed) return true; return sema_add_local(context, decl); FAIL_CHECK: if (check_failed) diff --git a/test/unit/stdlib/core/array.c3 b/test/unit/stdlib/core/array.c3 index 2c106a317..ce80f9ac7 100644 --- a/test/unit/stdlib/core/array.c3 +++ b/test/unit/stdlib/core/array.c3 @@ -1,25 +1,32 @@ module array_test; import std::io; -struct TestStructZip (Printable) +struct ArrTestStruct (Printable) { int a; int b; } -fn TestStructZip TestStructZip.mult(self, TestStructZip other) @operator(*) +fn ArrTestStruct ArrTestStruct.mult(self, ArrTestStruct other) @operator(*) { self.a *= other.a; self.b *= other.b; return self; } -fn bool TestStructZip.eq(self, TestStructZip other) @operator(==) +fn ArrTestStruct ArrTestStruct.add(self, ArrTestStruct other) @operator(+) +{ + self.a += other.a; + self.b += other.b; + return self; +} + +fn bool ArrTestStruct.equals(self, ArrTestStruct other) @operator(==) { return self.a == other.a && self.b == other.b; } -fn usz? TestStructZip.to_format(&self, Formatter* f) @dynamic +fn usz? ArrTestStruct.to_format(&self, Formatter* f) @dynamic { return f.printf("{ %d, %d }", self.a, self.b); } @@ -67,6 +74,110 @@ fn void concat() } +fn void reduce() => @pool() +{ + int[] int_slice = { 1, 8, 12 }; + test::eq(2 * 1 * 8 * 12, array::@reduce(int_slice, 2, fn (i, e, u) => i * e)); + test::eq(array::@product(int_slice, 2), array::@reduce(int_slice, 2, fn (i, e, u) => i * e)); + + String[] c3_is_great = { "hello,", " world.", " C3 ", "is ", "great!" }; + test::eq("Ahem! hello, world. C3 is great!", array::@reduce(c3_is_great, "Ahem! ", fn (i, e, u) => i.tconcat(e))); + + List {String} l; + l.push_all({ "abc", "adef", "ghi", "a12345" }); + test::eq(array::@reduce(l, (usz)0, fn (acc, e, u) => acc + e.len + u), 0 + (3 + 0) + (4 + 1) + (3 + 2) + (6 + 3)); +} + +fn void sum() +{ + test::eq(7, array::@sum((int[]){1, 2, 4})); + test::eq(12, array::@sum((int[]){1, 2, 4}, 5)); + test::eq(1, array::@sum((int[]){1, 2, 4}, -6)); + test::eq_approx(array::@sum((double[]){1.236, 2.38709, 4, -5.9}, 8.5), 10.223090); + test::eq((ArrTestStruct){10,10}, array::@sum((ArrTestStruct[]){{1,1}, {4,4}, {-2,7}, {17,8}}, (ArrTestStruct){-10,-10})); + + List {int} l; + l.push_all({ 1, 2, 3, 4 }); + test::eq(array::@sum(l), 10); +} + +fn void product() +{ + test::eq(0, array::@product((int[]){0, 23534, 2224})); + test::eq(64, array::@product((int[]){1, 2, 4, 8})); + test::eq_approx(array::@product((double[]){1, 2, 4, -0.5}, -3), 12.0); + test::eq((ArrTestStruct){1360,-2240}, array::@product((ArrTestStruct[]){{1,1}, {4,4}, {-2,7}, {17,8}}, (ArrTestStruct){-10,-10})); + + List {int} l; + l.push_all({ 1, 2, 3, 4 }); + test::eq(array::@product(l), 24); +} + +fn void indices_of() => @pool() +{ + int[] int_slice = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + test::eq(array::@tindices_of(int_slice, fn (e, u) => !!(e % 2)), (usz[]){0, 2, 4, 6, 8}); + test::eq(array::@tindices_of(int_slice, fn (e, u) => !(e % 2)), (usz[]){1, 3, 5, 7, 9}); + test::eq(array::@tindices_of(int_slice, fn (e, u) => e > 5), (usz[]){5, 6, 7, 8, 9}); + + ArrTestStruct[] struct_slice = { {40, 5}, {0, 0}, {12, 13}, {3, 5} }; + test::eq(array::@tindices_of(struct_slice, fn (e, u) => e.a > 10), (usz[]){0, 2}); + test::eq(array::@tindices_of(struct_slice, fn (e, u) => e.b == 0 || e.a < 10), (usz[]){1, 3}); + + List {String} l; + l.push_all({ "abc", "adef", "ghi", "a12345" }); + test::eq(array::@tindices_of(l, fn (e, u) => e.len <= 3), (usz[]){0, 2}); +} + +fn void filter() => @pool() +{ + int[] int_slice = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + test::eq(array::@tfilter(int_slice, fn (e, u) => !!(e % 2)), (int[]){1, 3, 5, 7, 9}); + test::eq(array::@tfilter(int_slice, fn (e, u) => !(e % 2)), (int[]){2, 4, 6, 8, 10}); + test::eq(array::@tfilter(int_slice, fn (e, u) => e > 5), (int[]){6, 7, 8, 9, 10}); + + ArrTestStruct[] struct_slice = { {40, 5}, {0, 0}, {12, 13}, {3, 5} }; + foreach (i, &e : array::@tfilter(struct_slice, fn (e, u) => e.a > 10)) test::eq(*e, (ArrTestStruct[2]){ {40, 5}, {12, 13} }[i]); + foreach (i, &e : array::@tfilter(struct_slice, fn (e, u) => e.b == 0 || e.a < 10)) test::eq(*e, (ArrTestStruct[2]){ {0, 0}, {3, 5} }[i]); + + List {String} l; + l.push_all({ "abc", "adef", "ghi", "a12345" }); + test::eq(array::@tfilter(l, fn (e, u) => e.len <= 3), (String[]){ "abc", "ghi" }); +} + +fn void any_true() => @pool() +{ + int[] int_slice = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + test::@check(array::@any(int_slice, fn (e, u) => e == 7)); + test::@check(!array::@any(int_slice, fn (e, u) => e > 10)); + + ArrTestStruct[] struct_slice = { {0, 0}, {1, 1} }; + test::@check(array::@any(struct_slice, fn (e, u) => !e.a)); + test::@check(!array::@any(struct_slice, fn (e, u) => e.b != e.a)); + + List {String} l; + l.push_all({ "abc", "adef", "ghi", "a12345" }); + test::@check(array::@any(l, fn (e, u) => e.len > 5)); + test::@check(!array::@any(l, fn (e, u) => e == "hi")); +} + +fn void all_true() => @pool() +{ + int[] int_slice = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + test::@check(array::@all(int_slice, fn (e, u) => e < 100)); + test::@check(!array::@all(int_slice, fn (e, u) => e > 5)); + + ArrTestStruct[] struct_slice = { {0, 0}, {1, 1} }; + test::@check(array::@all(struct_slice, fn (e, u) => e.a != 2)); + test::@check(!array::@all(struct_slice, fn (e, u) => (e.a + e.b) > 0)); + + List {String} l; + l.push_all({ "abc", "adef", "ghi", "a12345" }); + test::@check(array::@all(l, fn (e, u) => e.len > 1)); + test::@check(!array::@all(l, fn (e, u) => e.len > 3)); +} + + fn void zip() => @pool() { char[] left = "abcde"; @@ -138,11 +249,11 @@ fn void zip_fill_with_string() => @pool() fn void zip_fill_with_struct() => @pool() { String[] left = { "abcde", "123456", "zzz" }; - TestStructZip[] right = { {1, 2} }; + ArrTestStruct[] right = { {1, 2} }; - Pair{String, TestStructZip}[] expected = { {"abcde", {1, 2}}, {"123456", {100, 200}}, {"zzz", {100, 200}} }; + Pair{String, ArrTestStruct}[] expected = { {"abcde", {1, 2}}, {"123456", {100, 200}}, {"zzz", {100, 200}} }; - Pair{String, TestStructZip}[] zipped = array::@tzip(left, right, fill_with: (TestStructZip){100, 200}); + Pair{String, ArrTestStruct}[] zipped = array::@tzip(left, right, fill_with: (ArrTestStruct){100, 200}); test::eq(zipped.len, 3); foreach (i, c : zipped) test::@check(c == expected[i], "Mismatch on index %d: %s (actual) != %s (expected)", i, c, expected[i]); @@ -217,12 +328,12 @@ fn void zip_with_fill_with_string() => @pool() fn void zip_with_fill_with_struct() => @pool() { - TestStructZip[] left = { {1, 2}, {300, 400} }; - TestStructZip[] right = { {-1, -1} }; + ArrTestStruct[] left = { {1, 2}, {300, 400} }; + ArrTestStruct[] right = { {-1, -1} }; - TestStructZip[] expected = { {-1, -2}, {600, 1200} }; + ArrTestStruct[] expected = { {-1, -2}, {600, 1200} }; - TestStructZip[] zipped = array::@tzip(left, right, fn TestStructZip (TestStructZip a, TestStructZip b) => a * b, (TestStructZip){2, 3}); + ArrTestStruct[] zipped = array::@tzip(left, right, fn ArrTestStruct (ArrTestStruct a, ArrTestStruct b) => a * b, (ArrTestStruct){2, 3}); test::eq(zipped.len, 2); foreach (i, c : zipped) test::@check(c == expected[i], "Mismatch on index %d: %s (actual) != %s (expected)", i, c, expected[i]);