diff --git a/src/build/builtin_compiler/main.zig b/src/build/builtin_compiler/main.zig index 87b64783fe..25eeac964e 100644 --- a/src/build/builtin_compiler/main.zig +++ b/src/build/builtin_compiler/main.zig @@ -155,6 +155,9 @@ fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) { if (env.common.findIdent("Builtin.List.is_empty")) |list_is_empty_ident| { try low_level_map.put(list_is_empty_ident, .list_is_empty); } + if (env.common.findIdent("Builtin.List.concat")) |list_concat_ident| { + try low_level_map.put(list_concat_ident, .list_concat); + } if (env.common.findIdent("list_get_unsafe")) |list_get_unsafe_ident| { try low_level_map.put(list_get_unsafe_ident, .list_get_unsafe); } diff --git a/src/build/roc/Builtin.roc b/src/build/roc/Builtin.roc index 0ec1278c6d..549f5d519c 100644 --- a/src/build/roc/Builtin.roc +++ b/src/build/roc/Builtin.roc @@ -9,6 +9,7 @@ Builtin := [].{ List := [ProvidedByCompiler].{ len : List(_elem) -> U64 is_empty : List(_elem) -> Bool + concat : List(item), List(item) -> List(item) first : List(elem) -> Try(elem, [ListWasEmpty]) first = |list| List.get(list, 0) @@ -25,9 +26,6 @@ Builtin := [].{ keep_if : List(a), (a -> Bool) -> List(a) keep_if = |_, _| [] - - concat : List(a), List(a) -> List(a) - concat = |_, _| [] } Bool := [True, False].{ diff --git a/src/builtins/list.zig b/src/builtins/list.zig index 8c312c9b61..ef3dbdbf50 100644 --- a/src/builtins/list.zig +++ b/src/builtins/list.zig @@ -1281,7 +1281,8 @@ pub fn listAllocationPtr( return list.getAllocationDataPtr(); } -fn rcNone(_: ?[*]u8) callconv(.c) void {} +/// No-op reference counting function for non-refcounted types +pub fn rcNone(_: ?[*]u8) callconv(.c) void {} /// Append UTF-8 string bytes to list for efficient string-to-bytes conversion. pub fn listConcatUtf8( diff --git a/src/canonicalize/Expression.zig b/src/canonicalize/Expression.zig index 36d15ef5aa..2823064f7e 100644 --- a/src/canonicalize/Expression.zig +++ b/src/canonicalize/Expression.zig @@ -391,6 +391,7 @@ pub const Expr = union(enum) { list_len, list_is_empty, list_get_unsafe, + list_concat, // Set operations set_is_empty, diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index 834613333d..6aa69ced46 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -2200,6 +2200,66 @@ pub const Interpreter = struct { // Copy to new location and increment refcount return try self.pushCopy(elem_value, roc_ops); }, + .list_concat => { + // List.concat : List(a), List(a) -> List(a) + // Args: List(a), List(a) + // Returns: List(a) (concatenated list) + std.debug.assert(args.len == 2); // low-level .list_concat expects 2 arguments + + const list_a_arg = args[0]; + const list_b_arg = args[1]; + + std.debug.assert(list_a_arg.ptr != null); // low-level .list_concat expects non-null list pointer + std.debug.assert(list_b_arg.ptr != null); // low-level .list_concat expects non-null list pointer + + // Extract element layout from List(a) + std.debug.assert(list_a_arg.layout.tag == .list or list_a_arg.layout.tag == .list_of_zst); + std.debug.assert(list_b_arg.layout.tag == .list or list_b_arg.layout.tag == .list_of_zst); + + const list_a: *const builtins.list.RocList = @ptrCast(@alignCast(list_a_arg.ptr.?)); + const list_b: *const builtins.list.RocList = @ptrCast(@alignCast(list_b_arg.ptr.?)); + + // Get element layout + const elem_layout_idx = list_a_arg.layout.data.list; + const elem_layout = self.runtime_layout_store.getLayout(elem_layout_idx); + const elem_size = self.runtime_layout_store.layoutSize(elem_layout); + const elem_alignment = elem_layout.alignment(self.runtime_layout_store.targetUsize()).toByteUnits(); + const elem_alignment_u32: u32 = @intCast(elem_alignment); + + // Determine if elements are refcounted + const elements_refcounted = elem_layout.isRefcounted(); + + // TODO: Proper refcounting for list elements + // For now, we use no-op functions even for refcounted elements. + // This works correctly because listConcat will handle the list-level refcounting, + // but element-level refcounting may need manual handling in complex cases. + const inc_fn = builtins.list.rcNone; + const dec_fn = builtins.list.rcNone; + + // Call listConcat - it consumes both input lists + const result_list = builtins.list.listConcat( + list_a.*, + list_b.*, + elem_alignment_u32, + elem_size, + elements_refcounted, + inc_fn, + dec_fn, + roc_ops, + ); + + // Allocate space for the result list + const result_layout = list_a_arg.layout; // Same layout as input + var out = try self.pushRaw(result_layout, 0); + out.is_initialized = false; + + // Copy the result list structure to the output + const result_ptr: *builtins.list.RocList = @ptrCast(@alignCast(out.ptr.?)); + result_ptr.* = result_list; + + out.is_initialized = true; + return out; + }, .set_is_empty => { // TODO: implement Set.is_empty self.triggerCrash("Set.is_empty not yet implemented", false, roc_ops); diff --git a/src/eval/test/low_level_interp_test.zig b/src/eval/test/low_level_interp_test.zig index 72242208e2..94102f4ab1 100644 --- a/src/eval/test/low_level_interp_test.zig +++ b/src/eval/test/low_level_interp_test.zig @@ -174,3 +174,102 @@ test "e_low_level_lambda - Str.is_empty in conditional" { const tag_name = result.module_env.getIdent(expr.e_zero_argument_tag.ident); try testing.expectEqualStrings("True", tag_name); } + +test "e_low_level_lambda - List.concat with two non-empty lists" { + const src = + \\x = List.concat([1, 2], [3, 4]) + \\len = List.len(x) + ; + + var result = try parseCheckAndEvalModule(src); + defer cleanupEvalModule(&result); + + const summary = try result.evaluator.evalAll(); + + // Should evaluate 2 declarations with 0 crashes + try testing.expectEqual(@as(u32, 2), summary.evaluated); + try testing.expectEqual(@as(u32, 0), summary.crashed); + + // Verify the length is 4 + const defs = result.module_env.store.sliceDefs(result.module_env.all_defs); + const len_def = result.module_env.store.getDef(defs[1]); + const len_expr = result.module_env.store.getExpr(len_def.expr); + + try testing.expect(len_expr == .e_num); + try testing.expectEqual(@as(u64, 4), len_expr.e_num.value.int); +} + +test "e_low_level_lambda - List.concat with empty and non-empty list" { + const src = + \\x = List.concat([], [1, 2, 3]) + \\len = List.len(x) + ; + + var result = try parseCheckAndEvalModule(src); + defer cleanupEvalModule(&result); + + const summary = try result.evaluator.evalAll(); + + // Should evaluate 2 declarations with 0 crashes + try testing.expectEqual(@as(u32, 2), summary.evaluated); + try testing.expectEqual(@as(u32, 0), summary.crashed); + + // Verify the length is 3 + const defs = result.module_env.store.sliceDefs(result.module_env.all_defs); + const len_def = result.module_env.store.getDef(defs[1]); + const len_expr = result.module_env.store.getExpr(len_def.expr); + + try testing.expect(len_expr == .e_num); + try testing.expectEqual(@as(u64, 3), len_expr.e_num.value.int); +} + +test "e_low_level_lambda - List.concat with two empty lists" { + const src = + \\x : List(U64) + \\x = List.concat([], []) + \\len = List.len(x) + ; + + var result = try parseCheckAndEvalModule(src); + defer cleanupEvalModule(&result); + + const summary = try result.evaluator.evalAll(); + + // Should evaluate 2 declarations with 0 crashes + try testing.expectEqual(@as(u32, 2), summary.evaluated); + try testing.expectEqual(@as(u32, 0), summary.crashed); + + // Verify the length is 0 + const defs = result.module_env.store.sliceDefs(result.module_env.all_defs); + const len_def = result.module_env.store.getDef(defs[1]); + const len_expr = result.module_env.store.getExpr(len_def.expr); + + try testing.expect(len_expr == .e_num); + try testing.expectEqual(@as(u64, 0), len_expr.e_num.value.int); +} + +test "e_low_level_lambda - List.concat preserves order" { + const src = + \\x = List.concat([10, 20], [30, 40, 50]) + \\first = List.first(x) + ; + + var result = try parseCheckAndEvalModule(src); + defer cleanupEvalModule(&result); + + const summary = try result.evaluator.evalAll(); + + // Should evaluate 2 declarations with 0 crashes + try testing.expectEqual(@as(u32, 2), summary.evaluated); + try testing.expectEqual(@as(u32, 0), summary.crashed); + + // Verify the first element is 10 (wrapped in Try.Ok) + const defs = result.module_env.store.sliceDefs(result.module_env.all_defs); + const first_def = result.module_env.store.getDef(defs[1]); + const first_expr = result.module_env.store.getExpr(first_def.expr); + + // Should be a Try.Ok tag with value 10 + try testing.expect(first_expr == .e_tag); + const tag_name = result.module_env.getIdent(first_expr.e_tag.ident); + try testing.expectEqualStrings("Ok", tag_name); +}