Add List.concat

This commit is contained in:
Richard Feldman 2025-11-09 08:08:48 -05:00
parent 092cf67728
commit 380630c823
No known key found for this signature in database
6 changed files with 166 additions and 4 deletions

View file

@ -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);
}

View file

@ -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].{

View file

@ -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(

View file

@ -391,6 +391,7 @@ pub const Expr = union(enum) {
list_len,
list_is_empty,
list_get_unsafe,
list_concat,
// Set operations
set_is_empty,

View file

@ -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);

View file

@ -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);
}