From 702b63ddb755a37108905d24643e75c7fed383fd Mon Sep 17 00:00:00 2001 From: Zack Puhl Date: Sat, 16 Aug 2025 07:30:24 -0400 Subject: [PATCH] Add `array::zip` and Related Macros (#2370) * zip / zip_into * Deprecate `add_array` in favour of `push_all` on lists. * Add support for generic lists for zip. --------- Co-authored-by: Christoffer Lerno --- lib/std/collections/elastic_array.c3 | 36 ++- lib/std/collections/list.c3 | 20 +- lib/std/core/array.c3 | 229 ++++++++++++++++- lib/std/core/mem.c3 | 12 + lib/std/core/mem_allocator.c3 | 17 ++ lib/std/core/values.c3 | 1 + releasenotes.md | 3 + src/compiler/sema_expr.c | 5 +- test/test_suite/contracts/inout_macro.c3 | 2 +- test/unit/stdlib/collections/elastic_array.c3 | 22 +- test/unit/stdlib/collections/list.c3 | 24 +- test/unit/stdlib/core/array.c3 | 233 +++++++++++++++++- test/unit/stdlib/sort/countingsort.c3 | 2 +- test/unit/stdlib/sort/insertionsort.c3 | 2 +- test/unit/stdlib/sort/quicksort.c3 | 2 +- test/unit/stdlib/sort/sorted.c3 | 2 +- 16 files changed, 567 insertions(+), 45 deletions(-) diff --git a/lib/std/collections/elastic_array.c3 b/lib/std/collections/elastic_array.c3 index 297dd049a..aabc41e81 100644 --- a/lib/std/collections/elastic_array.c3 +++ b/lib/std/collections/elastic_array.c3 @@ -121,7 +121,24 @@ fn usz ElasticArray.add_all_to_limit(&self, ElasticArray* other_list) @param [in] array *> -fn usz ElasticArray.add_array_to_limit(&self, Type[] array) +fn usz ElasticArray.add_array_to_limit(&self, Type[] array) @deprecated("Use push_all_to_limit") +{ + if (!array.len) return 0; + foreach (i, &value : array) + { + if (self.size == MAX_SIZE) return array.len - i; + self.entries[self.size++] = *value; + } + return 0; +} + +<* + Add as many values from this array as possible, returning the + number of elements that didn't fit. + + @param [in] array +*> +fn usz ElasticArray.push_all_to_limit(&self, Type[] array) { if (!array.len) return 0; foreach (i, &value : array) @@ -139,7 +156,7 @@ fn usz ElasticArray.add_array_to_limit(&self, Type[] array) @require array.len + self.size <= MAX_SIZE : `Size would exceed max.` @ensure self.size >= array.len *> -fn void ElasticArray.add_array(&self, Type[] array) +fn void ElasticArray.add_array(&self, Type[] array) @deprecated("Use push_all") { if (!array.len) return; foreach (&value : array) @@ -148,6 +165,21 @@ fn void ElasticArray.add_array(&self, Type[] array) } } +<* + Add the values of an array to this list. + + @param [in] array + @require array.len + self.size <= MAX_SIZE : `Size would exceed max.` + @ensure self.size >= array.len +*> +fn void ElasticArray.push_all(&self, Type[] array) +{ + if (!array.len) return; + foreach (&value : array) + { + self.entries[self.size++] = *value; + } +} <* diff --git a/lib/std/collections/list.c3 b/lib/std/collections/list.c3 index c618e9cef..531bd6850 100644 --- a/lib/std/collections/list.c3 +++ b/lib/std/collections/list.c3 @@ -57,7 +57,7 @@ fn List* List.tinit(&self, usz initial_capacity = 16) fn List* List.init_with_array(&self, Allocator allocator, Type[] values) { self.init(allocator, values.len) @inline; - self.add_array(values) @inline; + self.push_all(values) @inline; return self; } @@ -70,7 +70,7 @@ fn List* List.init_with_array(&self, Allocator allocator, Type[] values) fn List* List.tinit_with_array(&self, Type[] values) { self.tinit(values.len) @inline; - self.add_array(values) @inline; + self.push_all(values) @inline; return self; } @@ -199,7 +199,21 @@ fn Type[] List.array_view(&self) @param [in] array @ensure self.size >= array.len *> -fn void List.add_array(&self, Type[] array) +fn void List.add_array(&self, Type[] array) @deprecated("Use push_all") +{ + if (!array.len) return; + self.reserve(array.len); + usz index = self.set_size(self.size + array.len); + self.entries[index : array.len] = array[..]; +} + +<* + Add the values of an array to this list. + + @param [in] array + @ensure self.size >= array.len +*> +fn void List.push_all(&self, Type[] array) { if (!array.len) return; self.reserve(array.len); diff --git a/lib/std/core/array.c3 b/lib/std/core/array.c3 index 440c01700..646c7d0f8 100644 --- a/lib/std/core/array.c3 +++ b/lib/std/core/array.c3 @@ -1,5 +1,7 @@ module std::core::array; + import std::core::array::slice; +import std::collections::pair; <* Returns true if the array contains at least one element, else false @@ -18,6 +20,7 @@ macro bool contains(array, element) return false; } + <* Return the first index of element found in the array, searching from the start. @@ -35,6 +38,7 @@ macro index_of(array, element) return NOT_FOUND?; } + <* Slice a 2d array and create a Slice2d from it. @@ -74,6 +78,7 @@ macro rindex_of(array, element) return NOT_FOUND?; } + <* Concatenate two arrays or slices, returning a slice containing the concatenation of them. @@ -99,6 +104,7 @@ macro concat(Allocator allocator, arr1, arr2) @nodiscard } return result; } + <* Concatenate two arrays or slices, returning a slice containing the concatenation of them, allocated using the temp allocator. @@ -110,4 +116,225 @@ macro concat(Allocator allocator, arr1, arr2) @nodiscard @require @typeis(arr1[0], $typeof(arr2[0])) : "Arrays must have the same type" @ensure return.len == arr1.len + arr2.len *> -macro tconcat(arr1, arr2) @nodiscard => concat(tmem, arr1, arr2); \ No newline at end of file +macro tconcat(arr1, arr2) @nodiscard => concat(tmem, arr1, arr2); + + +<* + 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 + will be collected up to the length of the LONGER array, with missing values in the shorter array being + assigned to the value of `fill_with`. Return array elements do not have to be of the same type. + + For example: + ```c3 + uint[] chosen_session_ids = server::get_random_sessions(instance)[:128]; + String[200] refreshed_session_keys = prng::new_keys_batch(); + + Pair { uint, String }[] sessions_meta = array::zip(mem, chosen_session_ids, refreshed_session_keys); + // The resulting Pair{}[] slice is then length of the shortest of the two arrays, so 128. + + foreach (i, &sess : sessions:meta) { + // distribute new session keys to associated instance IDs + } + ``` + + Or: + ```c3 + String[] client_names = server::online_usernames(instance); + uint128[] session_ids = server::user_keys(); + + // in this example, we 'know' ahead of time that 'session_ids' can only ever be SHORTER + // than 'client_names', but never longer, because it's possible new users have logged + // in without getting whatever this 'session ID' is delegated to them. + Pair { String, uint128 }[] zipped = array::tzip(client_names, session_ids, fill_with: uint128.max); + + server::refresh_session_keys_by_pair(zipped)!; + ``` + + ### When an `operation` is supplied... + Apply an operation to each element of two slices or arrays and return the results of + each operation into a newly allocated array. + + This essentially combines Iterable1 with Iterable2 using the `operation` functor. + + See the functional `zipWith` construct, which has a more appropriate name than, e.g., `map`; + a la: https://hackage.haskell.org/package/base-4.21.0.0/docs/Prelude.html#v:zipWith + + Similar to "normal" `zip`, this macro pads the shorter input array with a given `fill_with`, or + an empty value if one isn't supplied. This `fill_with` is supplied to the `operation` functor + _BEFORE_ calculating its result while zipping. + + For example: a functor of `fn char (char a, char b) => a + b` with a `fill_with` of 7, + where the `left` array is the shorter iterable, will put 7 into that lambda in each place + where `left` is being filled in during the zip operation. + + @param [&inout] allocator : "The allocator to use; default is the heap allocator." + @param [in] left : "The left-side array. These items will be placed as the First in each Pair" + @param [in] right : "The right-side array. These items will be placed as the Second in each Pair" + @param #operation : "The function to apply. Must have a signature of `$typeof(a) (a, b)`, where the type of 'a' and 'b' is the element type of left/right respectively." + @param fill_with : "The value used to fill or pad the shorter iterable to the length of the longer one while zipping." + + @require @is_valid_list(left) &&& @is_valid_list(right) : "Left and right sides must be integer indexable" + @require @is_valid_operation(#operation, left, right) : "The operator must take two parameters matching the elements of the left and right side" + @require @is_valid_fill(left, right, fill_with) : "The specified fill value does not match either the left or the right array's underlying type." + +*> +macro @zip(Allocator allocator, left, right, #operation = EMPTY_MACRO_SLOT, fill_with = EMPTY_MACRO_SLOT) @nodiscard +{ + var $LeftType = $typeof(left[0]); + var $RightType = $typeof(right[0]); + + var $Type = Pair { $LeftType, $RightType }; + bool $is_op = @is_valid_macro_slot(#operation); + $if $is_op: + $Type = $typeof(#operation).returns; + $endif + + + usz left_len = find_len(left); + usz right_len = find_len(right); + + $LeftType left_fill; + $RightType right_fill; + usz result_len = min(left_len, right_len); + bool $has_fill = @is_valid_macro_slot(fill_with); + $if $has_fill: + switch + { + case left_len > right_len: + $if !$defined(($RightType)fill_with): + unreachable(); + $else + right_fill = ($RightType)fill_with; + result_len = left_len; + $endif + case left_len < right_len: + $if !$defined(($LeftType)fill_with): + unreachable(); + $else + left_fill = ($LeftType)fill_with; + result_len = right_len; + $endif + } + $endif + + if (result_len == 0) return ($Type[]){}; + + $Type[] result = allocator::alloc_array(allocator, $Type, result_len); + + foreach (idx, &item : result) + { + $if $is_op: + var $LambdaType = $typeof(fn $Type ($LeftType a, $RightType b) => ($Type){}); + $LambdaType $operation = ($LambdaType)#operation; + $LeftType lval = idx >= left_len ? left_fill : left[idx]; + $RightType rval = idx >= right_len ? right_fill : right[idx]; + *item = $operation(lval, rval); + $else + *item = { + idx >= left_len ? left_fill : left[idx], + idx >= right_len ? right_fill : right[idx] + }; + $endif + } + + return result; +} + +<* + Array 'zip' using the temp allocator. + + @param [in] left : "The left-side array. These items will be placed as the First in each Pair" + @param [in] right : "The right-side array. These items will be placed as the Second in each Pair" + @param #operation : "The function to apply. Must have a signature of `$typeof(a) (a, b)`, where the type of 'a' and 'b' is the element type of left/right respectively." + @param fill_with : "The value used to fill or pad the shorter iterable to the length of the longer one while zipping." + + @require @is_valid_list(left) &&& @is_valid_list(right) : "Left and right sides must be integer indexable" + @require @is_valid_operation(#operation, left, right) : "The operator must take two parameters matching the elements of the left and right side" + @require @is_valid_fill(left, right, fill_with) : "The specified fill value does not match either the left or the right array's underlying type." + +*> +macro @tzip(left, right, #operation = EMPTY_MACRO_SLOT, fill_with = EMPTY_MACRO_SLOT) @nodiscard +{ + return @zip(tmem, left, right, #operation, fill_with); +} + + +<* + Apply an operation to each element of two slices or arrays and store the results of + each operation into the 'left' value. + + This is useful because no memory allocations are required in order to perform the operation. + + A good example of using this might be using algorithmic transformations on data in-place: + ``` + char[] partial_cipher = get_next_plaintext_block(); + + array::@zip_into( + partial_cipher[ENCRYPT_OFFSET:BASE_KEY.len], + BASE_KEY, + fn char (char a, char b) => a ^ (b * 5) % 37 + ); + ``` + + This parameterizes the lambda function with left (`partial_cipher`) and right (`BASE_KEY`) slice + elements and stores the end result in-place within the left slice. This is in contrast to a + regular `zip_with` which will create a cloned final result and return it. + + @param [inout] left : `Slice to store results of applied functor/operation.` + @param [in] right : `Slice to apply in the functor/operation.` + @param #operation : "The function to apply. Must have a signature of `$typeof(a) (a, b)`, where the type of 'a' and 'b' is the element type of left/right respectively." + + @require @is_valid_list(left) : "Expected a valid list" + @require @is_valid_list(right) : "Expected a valid list" + @require find_len(right) >= find_len(left) : `Right side length must be >= the destination (left) side length; consider using a sub-array of data for the assignment.` + @require @assignable_to(#operation, $typefrom(@zip_into_fn(left, right))) : "The functor must use the same types as the `left` and `right` inputs, and return a value of the `left` type." +*> +macro @zip_into(left, right, #operation) +{ + $typefrom(@zip_into_fn(left, right)) $operation = #operation; + + foreach (i, &v : left) *v = $operation(left[i], right[i]); +} + + +// --- helper functions + +macro typeid @zip_into_fn(#left, #right) @private @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 +{ + $switch: + $case @is_empty_macro_slot(#operation): + return true; + $case @typekind(#operation) != FUNC: + return false; + $default: + return $defined(#operation(#left[0], #right[0])); + $endswitch +} + +macro bool @is_valid_list(#expr) @const @private +{ + return $defined(#expr[0]) &&& ($defined(#expr.len) ||| $defined(#expr.len())); +} + +macro bool @is_valid_fill(left, right, fill_with) @private +{ + if (@is_empty_macro_slot(fill_with)) return true; + usz left_len = @select($defined(left.len()), left.len(), left.len); + usz right_len = @select($defined(right.len()), right.len(), right.len); + if (left_len == right_len) return true; + return left_len > right_len ? $defined(($typeof(right[0]))fill_with) : $defined(($typeof(left[0]))fill_with); +} + +macro usz find_len(list) +{ + $if $defined(list.len()): + return list.len(); + $else + return list.len; + $endif +} \ No newline at end of file diff --git a/lib/std/core/mem.c3 b/lib/std/core/mem.c3 index 588c03d3a..8bff0d47c 100644 --- a/lib/std/core/mem.c3 +++ b/lib/std/core/mem.c3 @@ -684,6 +684,12 @@ macro @clone(value) @builtin @nodiscard return allocator::clone(mem, value); } +<* + @param value : "The value to clone" + @return "A pointer to the cloned value" +*> +macro @clone_slice(value) @builtin @nodiscard => allocator::clone_slice(mem, value); + <* @param value : "The value to clone" @return "A pointer to the cloned value, which must be released using free_aligned" @@ -706,6 +712,12 @@ macro @tclone(value) @builtin @nodiscard $endif } +<* + @param value : "The value to clone" + @return "A pointer to the cloned value" +*> +macro @tclone_slice(value) @builtin @nodiscard => allocator::clone_slice(tmem, value); + fn void* malloc(usz size) @builtin @inline @nodiscard { return allocator::malloc(mem, size); diff --git a/lib/std/core/mem_allocator.c3 b/lib/std/core/mem_allocator.c3 index f410effec..8b2e4f68b 100644 --- a/lib/std/core/mem_allocator.c3 +++ b/lib/std/core/mem_allocator.c3 @@ -317,6 +317,23 @@ macro clone(Allocator allocator, value) @nodiscard return new(allocator, $typeof(value), value); } +<* + @param [&inout] allocator : "The allocator used to clone" + @param slice : "The slice to clone" + @return "A pointer to the cloned slice" + + @require @typekind(slice) == SLICE || @typekind(slice) == ARRAY +*> +macro clone_slice(Allocator allocator, slice) @nodiscard +{ + var $Type = $typeof(slice[0]); + + $Type[] new_arr = new_array(allocator, $Type, slice.len); + mem::copy(new_arr.ptr, &slice[0], slice.len * $Type.sizeof); + + return new_arr; +} + <* Clone overaligned values. Must be released using free_aligned. diff --git a/lib/std/core/values.c3 b/lib/std/core/values.c3 index 7ecdde928..5830370d2 100644 --- a/lib/std/core/values.c3 +++ b/lib/std/core/values.c3 @@ -51,6 +51,7 @@ macro @select(bool $bool, #value_1, #value_2) @builtin return #value_2; $endif } + macro promote_int_same(x, y) { $if @is_int(x): diff --git a/releasenotes.md b/releasenotes.md index b6ce7aa35..04189b9aa 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -12,6 +12,8 @@ - Switch available for types implementing `@operator(==)`. - `Type.is_eq` is now true for types with `==` overload. - Methods ignore visibility settings. +- Allow inout etc on untyped macro parameters even if they are not pointers. +- Deprecate `add_array` in favour of `push_all` on lists. ### Fixes - List.remove_at would incorrectly trigger ASAN. @@ -49,6 +51,7 @@ - Updated hash functions in default hash methods. - Added `FixedBlockPool` which is a memory pool for fixed size blocks. - Added the experimental `std::core::log` for logging. +- Added array `@zip` and `@zip_into` macros. #2370 - Updated termios bindings to use bitstructs and fixed some constants with incorrect values #2372 - Added libloaderapi to `std::os::win32`. diff --git a/src/compiler/sema_expr.c b/src/compiler/sema_expr.c index 3f7ce5cb4..65a65f9f6 100644 --- a/src/compiler/sema_expr.c +++ b/src/compiler/sema_expr.c @@ -2578,7 +2578,6 @@ bool sema_expr_analyse_macro_call(SemaContext *context, Expr *call_expr, Expr *s goto SKIP_LINK; } Decl *func = context->call_env.current_function; - ASSERT_SPAN(func, func); ASSERT_SPAN(func, func->resolved_attributes); if (!func->attrs_resolved) { @@ -2773,7 +2772,7 @@ bool sema_expr_analyse_macro_call(SemaContext *context, Expr *call_expr, Expr *s if (param->var.init_expr) { Type *param_type = param->type; - if (param_type && (param->var.out_param || param->var.not_null)) + if (param_type && param->var.type_info && (param->var.out_param || param->var.not_null)) { param_type = type_flatten(param_type); if (param_type->type_kind != TYPE_POINTER && param_type->type_kind != TYPE_SLICE && param_type->type_kind != TYPE_INTERFACE && param_type->type_kind != TYPE_ANY) @@ -2782,7 +2781,7 @@ bool sema_expr_analyse_macro_call(SemaContext *context, Expr *call_expr, Expr *s goto EXIT_FAIL; } } - if (param->var.not_null) + if (param->var.not_null && (param_type->type_kind == TYPE_POINTER || param_type->type_kind == TYPE_SLICE || param_type->type_kind == TYPE_INTERFACE || param_type->type_kind == TYPE_ANY)) { Expr *expr = expr_variable(param); Expr *binary = expr_new_expr(EXPR_BINARY, expr); diff --git a/test/test_suite/contracts/inout_macro.c3 b/test/test_suite/contracts/inout_macro.c3 index 9e3514186..fac5dcead 100644 --- a/test/test_suite/contracts/inout_macro.c3 +++ b/test/test_suite/contracts/inout_macro.c3 @@ -7,6 +7,6 @@ macro foo(x) => *x = 1; fn int main() { - foo(0); // #error: Expected a pointer + foo(0); // #error: x must be return 0; } \ No newline at end of file diff --git a/test/unit/stdlib/collections/elastic_array.c3 b/test/unit/stdlib/collections/elastic_array.c3 index 8e22ae982..2540b895d 100644 --- a/test/unit/stdlib/collections/elastic_array.c3 +++ b/test/unit/stdlib/collections/elastic_array.c3 @@ -7,7 +7,7 @@ alias PtrList = ElasticArray{void*, 10}; fn void delete_contains_index() { IntList test; - test.add_array({ 1, 2 }); + test.push_all({ 1, 2 }); assert(test.contains(1)); assert(test.contains(2)); assert(!test.contains(0)); @@ -36,7 +36,7 @@ fn void delete_contains_index() fn void compact() { PtrList test; - test.add_array({ null, &test }); + test.push_all({ null, &test }); assert(test.compact_count() == 1); test.push(null); assert(test.compact_count() == 1); @@ -50,7 +50,7 @@ fn void reverse() { IntList test; test.reverse(); - test.add_array({ 1, 2 }); + test.push_all({ 1, 2 }); test.push(3); assert(test.array_view() == (int[]) { 1, 2, 3}); test.reverse(); @@ -66,13 +66,13 @@ fn void remove_if() IntList test; usz removed; - test.add_array({ 1, 11, 2, 10, 20 }); + test.push_all({ 1, 11, 2, 10, 20 }); removed = test.remove_if(&filter); assert(removed == 3); assert(test.array_view() == (int[]){1, 2}); test.clear(); - test.add_array({ 1, 11, 2, 10, 20 }); + test.push_all({ 1, 11, 2, 10, 20 }); removed = test.remove_if(&select); assert(removed == 2); assert(test.array_view() == (int[]){11, 10, 20}); @@ -84,13 +84,13 @@ fn void remove_using_test() IntList test; usz removed; - test.add_array({ 1, 11, 2, 10, 20 }); + test.push_all({ 1, 11, 2, 10, 20 }); removed = test.remove_using_test(fn bool(i, ctx) => *i >= *(int*)ctx, &&10); assert(removed == 3); assert(test.array_view() == (int[]){1, 2}); test.clear(); - test.add_array({ 1, 11, 2, 10, 20 }); + test.push_all({ 1, 11, 2, 10, 20 }); removed = test.remove_using_test(fn bool(i, ctx) => *i < *(int*)ctx, &&10); assert(removed == 2); assert(test.array_view() == (int[]){11, 10, 20}); @@ -101,13 +101,13 @@ fn void retain_if() IntList test; usz removed; - test.add_array({ 1, 11, 2, 10, 20 }); + test.push_all({ 1, 11, 2, 10, 20 }); removed = test.retain_if(&select); assert(removed == 3); assert(test.array_view() == (int[]){1, 2}); test.clear(); - test.add_array({ 1, 11, 2, 10, 20 }); + test.push_all({ 1, 11, 2, 10, 20 }); removed = test.retain_if(&filter); assert(removed == 2); assert(test.array_view() == (int[]){11, 10, 20}); @@ -118,13 +118,13 @@ fn void retain_using_test() IntList test; usz removed; - test.add_array({ 1, 11, 2, 10, 20 }); + test.push_all({ 1, 11, 2, 10, 20 }); removed = test.remove_using_test(fn bool(i, ctx) => *i >= *(int*)ctx, &&10); assert(removed == 3); assert(test.array_view() == (int[]){1, 2}); test.clear(); - test.add_array({ 1, 11, 2, 10, 20 }); + test.push_all({ 1, 11, 2, 10, 20 }); removed = test.remove_using_test(fn bool(i, ctx) => *i < *(int*)ctx, &&10); assert(removed == 2); assert(test.array_view() == (int[]){11, 10, 20}); diff --git a/test/unit/stdlib/collections/list.c3 b/test/unit/stdlib/collections/list.c3 index 6e5fcbc1f..55ff2ed10 100644 --- a/test/unit/stdlib/collections/list.c3 +++ b/test/unit/stdlib/collections/list.c3 @@ -25,7 +25,7 @@ fn void remove_at() IntList test; test.init(mem); defer test.free(); - test.add_array({ 1, 2, 3, 4 }); + test.push_all({ 1, 2, 3, 4 }); test::eq(test.array_view(), (int[]){ 1, 2, 3, 4 }); test.remove_at(0); test::eq(test.array_view(), (int[]){ 2, 3, 4 }); @@ -41,7 +41,7 @@ fn void delete_contains_index() { IntList test; - test.add_array({ 1, 2 }); + test.push_all({ 1, 2 }); assert(test.contains(1)); assert(test.contains(2)); assert(!test.contains(0)); @@ -70,7 +70,7 @@ fn void delete_contains_index() fn void compact() { PtrList test; - test.add_array({ null, &test }); + test.push_all({ null, &test }); assert(test.compact_count() == 1); test.push(null); assert(test.compact_count() == 1); @@ -85,7 +85,7 @@ fn void reverse() IntList test; test.reverse(); - test.add_array({ 1, 2 }); + test.push_all({ 1, 2 }); test.push(3); assert(test.array_view() == { 1, 2, 3}); test.reverse(); @@ -101,13 +101,13 @@ fn void remove_if() IntList test; usz removed; - test.add_array({ 1, 11, 2, 10, 20 }); + test.push_all({ 1, 11, 2, 10, 20 }); removed = test.remove_if(&filter); assert(removed == 3); assert(test.array_view() == {1, 2}); test.clear(); - test.add_array({ 1, 11, 2, 10, 20 }); + test.push_all({ 1, 11, 2, 10, 20 }); removed = test.remove_if(&select); assert(removed == 2); assert(test.array_view() == {11, 10, 20}); @@ -135,13 +135,13 @@ fn void remove_using_test() IntList test; usz removed; - test.add_array({ 1, 11, 2, 10, 20 }); + test.push_all({ 1, 11, 2, 10, 20 }); removed = test.remove_using_test(fn bool(i, ctx) => *i >= *(int*)ctx, &&10); assert(removed == 3); assert(test.array_view() == {1, 2}); test.clear(); - test.add_array({ 1, 11, 2, 10, 20 }); + test.push_all({ 1, 11, 2, 10, 20 }); removed = test.remove_using_test(fn bool(i, ctx) => *i < *(int*)ctx, &&10); assert(removed == 2); assert(test.array_view() == {11, 10, 20}); @@ -152,13 +152,13 @@ fn void retain_if() IntList test; usz removed; - test.add_array({ 1, 11, 2, 10, 20 }); + test.push_all({ 1, 11, 2, 10, 20 }); removed = test.retain_if(&select); assert(removed == 3); assert(test.array_view() == {1, 2}); test.clear(); - test.add_array({ 1, 11, 2, 10, 20 }); + test.push_all({ 1, 11, 2, 10, 20 }); removed = test.retain_if(&filter); assert(removed == 2); assert(test.array_view() == {11, 10, 20}); @@ -169,13 +169,13 @@ fn void retain_using_test() IntList test; usz removed; - test.add_array({ 1, 11, 2, 10, 20 }); + test.push_all({ 1, 11, 2, 10, 20 }); removed = test.remove_using_test(fn bool(i, ctx) => *i >= *(int*)ctx, &&10); assert(removed == 3); assert(test.array_view() == {1, 2}); test.clear(); - test.add_array({ 1, 11, 2, 10, 20 }); + test.push_all({ 1, 11, 2, 10, 20 }); removed = test.remove_using_test(fn bool(i, ctx) => *i < *(int*)ctx, &&10); assert(removed == 2); assert(test.array_view() == {11, 10, 20}); diff --git a/test/unit/stdlib/core/array.c3 b/test/unit/stdlib/core/array.c3 index f05beb051..2c106a317 100644 --- a/test/unit/stdlib/core/array.c3 +++ b/test/unit/stdlib/core/array.c3 @@ -1,19 +1,47 @@ -module arraytests @test; +module array_test; +import std::io; + +struct TestStructZip (Printable) +{ + int a; + int b; +} + +fn TestStructZip TestStructZip.mult(self, TestStructZip other) @operator(*) +{ + self.a *= other.a; + self.b *= other.b; + return self; +} + +fn bool TestStructZip.eq(self, TestStructZip other) @operator(==) +{ + return self.a == other.a && self.b == other.b; +} + +fn usz? TestStructZip.to_format(&self, Formatter* f) @dynamic +{ + return f.printf("{ %d, %d }", self.a, self.b); +} + +module array_test @test; +import std::collections::pair, std::collections::list; + fn void contains() { int[3] a = { 1, 2, 3 }; - assert(array::contains(a, 2) == true); - assert(array::contains(a, 15) == false); + assert(array::contains(a, 2)); + assert(!array::contains(a, 15)); } fn void find() { int[3] a = { 1, 2, 3 }; - assert(array::index_of(a, 2)!! == 1); - assert(array::index_of(a, 1)!! == 0); - assert(array::index_of(a, 3)!! == 2); - assert(@catch(array::index_of(a, 4)) == NOT_FOUND); + test::eq(array::index_of(a, 2)!!, 1); + test::eq(array::index_of(a, 1)!!, 0); + test::eq(array::index_of(a, 3)!!, 2); + test::@error(array::index_of(a, 4), NOT_FOUND); } fn void find_subarray() @@ -35,5 +63,194 @@ fn void concat() free(array::concat(mem, a[:0], (int[2]) { 1, 2 })); int[] c = array::concat(mem, a[1..2], a); defer free(c); - assert (c == (int[]){ 2, 3, 1, 2, 3 }); + assert(c == (int[]){ 2, 3, 1, 2, 3 }); +} + + +fn void zip() => @pool() +{ + char[] left = "abcde"; + long[] right = { -1, 0x8000, 0 }; + + Pair{char, long}[] expected = { {'a', -1}, {'b', 0x8000}, {'c', 0} }; + + Pair{char, long}[] zipped = array::@tzip(left, right); + + test::eq(zipped.len, 3); + foreach (i, c : zipped) assert(c == expected[i], "Mismatch on index %d: %s (actual) != %s (expected)", i, c, expected[i]); +} + +fn void zip_list() => @pool() +{ + char[] left = "abcde"; + List{long} l; + l.push(-1); + l.push(0x8000); + l.push(0); + + Pair{char, long}[] expected = { {'a', -1}, {'b', 0x8000}, {'c', 0} }; + + Pair{char, long}[] zipped = array::@tzip(left, l); + + test::eq(zipped.len, 3); + foreach (i, c : zipped) assert(c == expected[i], "Mismatch on index %d: %s (actual) != %s (expected)", i, c, expected[i]); +} + +fn void zip_fill_with_default() => @pool() +{ + char[] left = "abcde"; + long[] right = { -1, 0x8000, 0 }; + + Pair{char, long}[] expected = { {'a', -1}, {'b', 0x8000}, {'c', 0}, {'d', 0}, {'e', 0} }; + + Pair{char, long}[] zipped = array::@tzip(left, right, fill_with: 0); + + test::eq(zipped.len, 5); + foreach (i, c : zipped) test::@check(c == expected[i], "Mismatch on index %d: %s (actual) != %s (expected)", i, c, expected[i]); +} + +fn void zip_fill_with_char() => @pool() +{ + char[] left = "abcde"; + long[] right = { -1, 0x8000, 0 }; + + Pair{char, long}[] expected = { {'a', -1}, {'b', 0x8000}, {'c', 0}, {'d', 0x40}, {'e', 0x40} }; + + Pair{char, long}[] zipped = array::@tzip(left, right, fill_with: 0x40); + + test::eq(zipped.len, 5); + foreach (i, c : zipped) test::@check(c == expected[i], "Mismatch on index %d: %s (actual) != %s (expected)", i, c, expected[i]); +} + +fn void zip_fill_with_string() => @pool() +{ + String[] left = { "abcde", "123456" }; + long[] right = { -1, 0x8000, 20, 30, 40 }; + + Pair{String, long}[] expected = { {"abcde", -1}, {"123456", 0x8000}, {"aaa", 20}, {"aaa", 30}, {"aaa", 40} }; + + Pair{String, long}[] zipped = array::@tzip(left, right, fill_with: "aaa"); + + test::eq(zipped.len, 5); + foreach (i, c : zipped) test::@check(c == expected[i], "Mismatch on index %d: %s (actual) != %s (expected)", i, c, expected[i]); +} + +fn void zip_fill_with_struct() => @pool() +{ + String[] left = { "abcde", "123456", "zzz" }; + TestStructZip[] right = { {1, 2} }; + + Pair{String, TestStructZip}[] expected = { {"abcde", {1, 2}}, {"123456", {100, 200}}, {"zzz", {100, 200}} }; + + Pair{String, TestStructZip}[] zipped = array::@tzip(left, right, fill_with: (TestStructZip){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]); +} + +fn void zip_with() => @pool() +{ + char[] left = "abcde"; + char[4] right = { 0x05, 0x04, 0x03, 0x00 }; + + char[] expected = "fffd"; + + char[] zipped = array::@tzip(left, right, fn char (char a, char b) => a + b); + + test::eq(zipped.len, 4); + foreach (i, c : zipped) test::@check(c == expected[i], "Mismatch on index %d: %s (actual) != %s (expected)", i, c, expected[i]); +} + +fn void zip_with_fill_with_default() => @pool() +{ + char[] left = "abcde"; + char[] right = { 0x05, 0x04 }; + + char[] expected = "ffcde"; + + char[] zipped = array::@tzip(left, right, fn char (char a, char b) => a + b, 0); + + test::eq(zipped.len, 5); + foreach (i, c : zipped) test::@check(c == expected[i], "Mismatch on index %d: %s (actual) != %s (expected)", i, c, expected[i]); +} + +fn void zip_with_fill_with_char() => @pool() +{ + char[] left = "abcde"; + char[] right = { 0x05, 0x04 }; + + char[] expected = "ffghi"; + + char[] zipped = array::@tzip(left, right, fn char (char a, char b) => a + b, 0x04); + + test::eq(zipped.len, 5); + foreach (i, c : zipped) test::@check(c == expected[i], "Mismatch on index %d: %s (actual) != %s (expected)", i, c, expected[i]); +} + +fn void zip_with_fill_with_pointers() => @pool() +{ + ZString field = "0123456789abcdefghijklmnopqrstuvwxyz-_=!"; + + char*[] left = { &field[3], &field[1] }; + char[] right = { 0x05, 0x04, 0x0A, 0x10, 0x11 }; + + char[] expected = "85agh"; + + char[] zipped = array::@tzip(left, right, fn char (char* a, char b) => a[b], &field[0]); + + test::eq(zipped.len, 5); + foreach (i, c : zipped) test::@check(c == expected[i], "Mismatch on index %d: %s (actual) != %s (expected)", i, c, expected[i]); +} + +fn void zip_with_fill_with_string() => @pool() +{ + String[] left = { "Hello", "World", "Foo", "Bazzy" }; + String[] right = { " there", "!" }; + + String[] expected = { "Hello there", "World!", "FooBar", "BazzyBar" }; + + String[] zipped = array::@tzip(left, right, fn String (String a, String b) => a.tconcat(b), "Bar"); + + test::eq(zipped.len, 4); + foreach (i, c : zipped) test::@check(c == expected[i], "Mismatch on index %d: %s (actual) != %s (expected)", i, c, expected[i]); +} + +fn void zip_with_fill_with_struct() => @pool() +{ + TestStructZip[] left = { {1, 2}, {300, 400} }; + TestStructZip[] right = { {-1, -1} }; + + TestStructZip[] expected = { {-1, -2}, {600, 1200} }; + + TestStructZip[] zipped = array::@tzip(left, right, fn TestStructZip (TestStructZip a, TestStructZip b) => a * b, (TestStructZip){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]); +} + +fn void zip_into() +{ + char[] left = { '1', '2', '3', '4' }; + String[6] right = { "one", "two", "three", "four", "five", "six" }; + + char[] expected = { '4', '5', '8', '8' }; + + array::@zip_into(left, right, fn (a, b) => a + (char)b.len); + + test::eq(left.len, 4); + foreach (i, c : left) test::@check(c == expected[i], "Mismatch on index %d: %s (actual) != %s (expected)", i, c, expected[i]); +} + +fn void zip_into_list() => @pool() +{ + List{char} l; + l.push_all({ '1', '2', '3', '4' }); + String[6] right = { "one", "two", "three", "four", "five", "six" }; + + char[] expected = { '4', '5', '8', '8' }; + + array::@zip_into(l, right, fn (a, b) => a + (char)b.len); + + test::eq(l.len(), 4); + foreach (i, c : l) test::@check(c == expected[i], "Mismatch on index %d: %s (actual) != %s (expected)", i, c, expected[i]); } diff --git a/test/unit/stdlib/sort/countingsort.c3 b/test/unit/stdlib/sort/countingsort.c3 index 2e9e21a41..399090266 100644 --- a/test/unit/stdlib/sort/countingsort.c3 +++ b/test/unit/stdlib/sort/countingsort.c3 @@ -78,7 +78,7 @@ alias CountingSortTestList = List{int}; fn void countingsort_list() { CountingSortTestList list; - list.add_array({ 2, 1, 3}); + list.push_all({ 2, 1, 3}); sort::countingsort(list, &sort::key_int_value); assert(check::int_ascending_sort(list.array_view())); } diff --git a/test/unit/stdlib/sort/insertionsort.c3 b/test/unit/stdlib/sort/insertionsort.c3 index 7de3b7156..31d4b1aff 100644 --- a/test/unit/stdlib/sort/insertionsort.c3 +++ b/test/unit/stdlib/sort/insertionsort.c3 @@ -83,7 +83,7 @@ alias InsertionSortTestList = List{int}; fn void insertionsort_list() { InsertionSortTestList list; - list.add_array({ 2, 1, 3}); + list.push_all({ 2, 1, 3}); sort::insertionsort(list, &sort::cmp_int_value); assert(check::int_ascending_sort(list.array_view())); } diff --git a/test/unit/stdlib/sort/quicksort.c3 b/test/unit/stdlib/sort/quicksort.c3 index 8cdc7e004..60e134301 100644 --- a/test/unit/stdlib/sort/quicksort.c3 +++ b/test/unit/stdlib/sort/quicksort.c3 @@ -83,7 +83,7 @@ alias List = List{int}; fn void quicksort_list() { List list; - list.add_array({ 2, 1, 3}); + list.push_all({ 2, 1, 3}); sort::quicksort(list, &sort::cmp_int_value); assert(check::int_sort(list.array_view())); } diff --git a/test/unit/stdlib/sort/sorted.c3 b/test/unit/stdlib/sort/sorted.c3 index 91950acc6..318b95479 100644 --- a/test/unit/stdlib/sort/sorted.c3 +++ b/test/unit/stdlib/sort/sorted.c3 @@ -68,7 +68,7 @@ fn void sorted() // with list List{int} list; list.tinit(); - list.add_array(tc.input); + list.push_all(tc.input); got = is_sorted(list); assert(got == tc.want, "list: %s, got: %s, want: %s",