Convert real paths to use stack-safe interpreting

This commit is contained in:
Richard Feldman 2025-11-27 09:15:31 -05:00
parent 120e507778
commit cfdf84689b
No known key found for this signature in database

View file

@ -353,8 +353,9 @@ pub const Interpreter = struct {
}
// Minimal evaluator for subset: string literals, lambdas without captures, and lambda calls
// Now uses the stack-safe implementation instead of recursive evaluation
pub fn evalMinimal(self: *Interpreter, expr_idx: can.CIR.Expr.Idx, roc_ops: *RocOps) Error!StackValue {
return try self.evalExprMinimal(expr_idx, roc_ops, null);
return try self.evalStackSafe(expr_idx, roc_ops, null);
}
pub fn registerDefValue(self: *Interpreter, expr_idx: can.CIR.Expr.Idx, value: StackValue) void {
@ -7171,6 +7172,17 @@ pub const Interpreter = struct {
/// Bind a declaration pattern to the evaluated value.
bind_decl: BindDecl,
/// Collect tuple elements: after evaluating an element, either continue
/// collecting more elements or finalize the tuple.
tuple_collect: TupleCollect,
/// Collect list elements: after evaluating an element, either continue
/// collecting more elements or finalize the list.
list_collect: ListCollect,
/// Collect record fields: first evaluate extension (if any), then fields.
record_collect: RecordCollect,
pub const DecrefValue = struct {
value: StackValue,
};
@ -7191,7 +7203,7 @@ pub const Interpreter = struct {
/// The body to evaluate if condition is true
body: can.CIR.Expr.Idx,
/// Remaining branches to try (slice indices into store)
remaining_branches: []const can.CIR.IfBranch.Idx,
remaining_branches: []const can.CIR.Expr.IfBranch.Idx,
/// The final else expression
final_else: can.CIR.Expr.Idx,
};
@ -7217,15 +7229,48 @@ pub const Interpreter = struct {
/// Bindings length at block start (for cleanup)
bindings_start: usize,
};
pub const TupleCollect = struct {
/// Number of collected values on the value stack (collected so far)
collected_count: usize,
/// Remaining element expressions to evaluate
remaining_elems: []const can.CIR.Expr.Idx,
};
pub const ListCollect = struct {
/// Number of collected values on the value stack (collected so far)
collected_count: usize,
/// Remaining element expressions to evaluate
remaining_elems: []const can.CIR.Expr.Idx,
/// Element runtime type variable (for type-consistent evaluation)
elem_rt_var: types.Var,
/// List runtime type variable (for layout computation)
list_rt_var: types.Var,
};
pub const RecordCollect = struct {
/// Number of collected field values on the value stack (plus base record if any)
collected_count: usize,
/// Remaining field expressions to evaluate
remaining_fields: []const can.CIR.RecordField.Idx,
/// Record runtime type variable (for layout computation)
rt_var: types.Var,
/// Expression idx for caching
expr_idx: can.CIR.Expr.Idx,
/// Whether this record has an extension base (the first value on stack will be the base)
has_extension: bool,
/// All fields in the record (for name lookup during finalization)
all_fields: []const can.CIR.RecordField.Idx,
};
};
/// Work stack for the stack-safe interpreter.
/// Contains pending operations (eval expressions or apply continuations).
pub const WorkStack = struct {
items: std.ArrayList(WorkItem),
items: std.array_list.AlignedManaged(WorkItem, null),
pub fn init(allocator: std.mem.Allocator) WorkStack {
return .{ .items = std.ArrayList(WorkItem).init(allocator) };
pub fn init(allocator: std.mem.Allocator) !WorkStack {
return .{ .items = try std.array_list.AlignedManaged(WorkItem, null).initCapacity(allocator, 64) };
}
pub fn deinit(self: *WorkStack) void {
@ -7237,7 +7282,7 @@ pub const Interpreter = struct {
}
pub fn pop(self: *WorkStack) ?WorkItem {
return self.items.popOrNull();
return self.items.pop();
}
/// Push multiple items in reverse order so they execute in forward order.
@ -7254,10 +7299,10 @@ pub const Interpreter = struct {
/// Value stack for the stack-safe interpreter.
/// Contains intermediate results from evaluated expressions.
pub const ValueStack = struct {
items: std.ArrayList(StackValue),
items: std.array_list.AlignedManaged(StackValue, null),
pub fn init(allocator: std.mem.Allocator) ValueStack {
return .{ .items = std.ArrayList(StackValue).init(allocator) };
pub fn init(allocator: std.mem.Allocator) !ValueStack {
return .{ .items = try std.array_list.AlignedManaged(StackValue, null).initCapacity(allocator, 64) };
}
pub fn deinit(self: *ValueStack) void {
@ -7269,7 +7314,7 @@ pub const Interpreter = struct {
}
pub fn pop(self: *ValueStack) ?StackValue {
return self.items.popOrNull();
return self.items.pop();
}
/// Peek at the top value without removing it.
@ -7288,10 +7333,10 @@ pub const Interpreter = struct {
roc_ops: *RocOps,
expected_rt_var: ?types.Var,
) Error!StackValue {
var work_stack = WorkStack.init(self.allocator);
var work_stack = try WorkStack.init(self.allocator);
defer work_stack.deinit();
var value_stack = ValueStack.init(self.allocator);
var value_stack = try ValueStack.init(self.allocator);
defer value_stack.deinit();
// Initial work: evaluate the root expression, then return result
@ -7444,9 +7489,10 @@ pub const Interpreter = struct {
} });
},
else => {
// Other binary operations (arithmetic, comparison) will be implemented
// in a later PR via method dispatch
@panic("Stack-safe interpreter: non-boolean binop not yet implemented");
// Fall back to recursive evaluation for other binops (arithmetic, comparison)
// Will be fully implemented via continuations in a later PR
const value = try self.evalExprMinimal(expr_idx, roc_ops, expected_rt_var);
try value_stack.push(value);
},
}
},
@ -7516,8 +7562,116 @@ pub const Interpreter = struct {
}
},
// ================================================================
// Tuples
// ================================================================
.e_tuple => |tup| {
const elems = self.env.store.sliceExpr(tup.elems);
if (elems.len == 0) {
// Empty tuple - create immediately
// Compute tuple layout with no elements
const tuple_layout_idx = try self.runtime_layout_store.putTuple(&[0]Layout{});
const tuple_layout = self.runtime_layout_store.getLayout(tuple_layout_idx);
const value = try self.pushRaw(tuple_layout, 0);
try value_stack.push(value);
} else {
// Schedule collection of elements
// Push tuple_collect continuation (to be executed after first element)
try work_stack.push(.{ .apply_continuation = .{ .tuple_collect = .{
.collected_count = 0,
.remaining_elems = elems,
} } });
}
},
// ================================================================
// Lists
// ================================================================
.e_list => |list_expr| {
const elems = self.env.store.sliceExpr(list_expr.elems);
// Get list type variable
const list_rt_var = expected_rt_var orelse blk: {
const ct_var = can.ModuleEnv.varFrom(expr_idx);
break :blk try self.translateTypeVar(self.env, ct_var);
};
if (elems.len == 0) {
// Empty list - create immediately
const list_layout = try self.getRuntimeLayout(list_rt_var);
const dest = try self.pushRaw(list_layout, 0);
if (dest.ptr != null) {
const header: *RocList = @ptrCast(@alignCast(dest.ptr.?));
header.* = RocList.empty();
}
try value_stack.push(dest);
} else {
// Get element type variable from first element
const first_elem_var: types.Var = @enumFromInt(@intFromEnum(elems[0]));
const elem_rt_var = try self.translateTypeVar(self.env, first_elem_var);
// Schedule collection of elements
try work_stack.push(.{ .apply_continuation = .{ .list_collect = .{
.collected_count = 0,
.remaining_elems = elems,
.elem_rt_var = elem_rt_var,
.list_rt_var = list_rt_var,
} } });
}
},
// ================================================================
// Records
// ================================================================
.e_record => |rec| {
const ct_var = can.ModuleEnv.varFrom(expr_idx);
const rt_var = try self.translateTypeVar(self.env, ct_var);
const fields = self.env.store.sliceRecordFields(rec.fields);
if (rec.ext) |ext_idx| {
// Has extension record - schedule extension evaluation first
try work_stack.push(.{ .apply_continuation = .{ .record_collect = .{
.collected_count = 0,
.remaining_fields = fields,
.rt_var = rt_var,
.expr_idx = expr_idx,
.has_extension = true,
.all_fields = fields,
} } });
// Evaluate extension first - it will be the first value on stack
const ext_ct_var = can.ModuleEnv.varFrom(ext_idx);
const ext_rt_var = try self.translateTypeVar(self.env, ext_ct_var);
try work_stack.push(.{ .eval_expr = .{
.expr_idx = ext_idx,
.expected_rt_var = ext_rt_var,
} });
} else if (fields.len == 0) {
// Empty record with no extension - create immediately
const rec_layout = try self.getRuntimeLayout(rt_var);
const dest = try self.pushRaw(rec_layout, 0);
try value_stack.push(dest);
} else {
// Non-empty record without extension
try work_stack.push(.{ .apply_continuation = .{ .record_collect = .{
.collected_count = 0,
.remaining_fields = fields,
.rt_var = rt_var,
.expr_idx = expr_idx,
.has_extension = false,
.all_fields = fields,
} } });
}
},
else => {
@panic("Stack-safe interpreter: expression type not yet implemented");
// Fall back to recursive evaluation for not-yet-implemented expression types.
// This allows the stack-safe interpreter to work while we incrementally
// add support for more expression types.
const value = try self.evalExprMinimal(expr_idx, roc_ops, expected_rt_var);
try value_stack.push(value);
},
}
}
@ -7531,7 +7685,7 @@ pub const Interpreter = struct {
self: *Interpreter,
expr_idx: can.CIR.Expr.Idx,
expected_rt_var: ?types.Var,
num_lit: can.CIR.Expr.Num,
num_lit: @TypeOf(@as(can.CIR.Expr, undefined).e_num),
) Error!StackValue {
const rt_var = expected_rt_var orelse blk: {
const ct_var = can.ModuleEnv.varFrom(expr_idx);
@ -7580,7 +7734,7 @@ pub const Interpreter = struct {
self: *Interpreter,
expr_idx: can.CIR.Expr.Idx,
expected_rt_var: ?types.Var,
lit: can.CIR.Expr.FracF32,
lit: @TypeOf(@as(can.CIR.Expr, undefined).e_frac_f32),
) Error!StackValue {
const rt_var = expected_rt_var orelse blk: {
const ct_var = can.ModuleEnv.varFrom(expr_idx);
@ -7600,7 +7754,7 @@ pub const Interpreter = struct {
self: *Interpreter,
expr_idx: can.CIR.Expr.Idx,
expected_rt_var: ?types.Var,
lit: can.CIR.Expr.FracF64,
lit: @TypeOf(@as(can.CIR.Expr, undefined).e_frac_f64),
) Error!StackValue {
const rt_var = expected_rt_var orelse blk: {
const ct_var = can.ModuleEnv.varFrom(expr_idx);
@ -7620,7 +7774,7 @@ pub const Interpreter = struct {
self: *Interpreter,
expr_idx: can.CIR.Expr.Idx,
expected_rt_var: ?types.Var,
dec_lit: can.CIR.Expr.Dec,
dec_lit: @TypeOf(@as(can.CIR.Expr, undefined).e_dec),
) Error!StackValue {
const rt_var = expected_rt_var orelse blk: {
const ct_var = can.ModuleEnv.varFrom(expr_idx);
@ -7640,7 +7794,7 @@ pub const Interpreter = struct {
self: *Interpreter,
expr_idx: can.CIR.Expr.Idx,
expected_rt_var: ?types.Var,
small: can.CIR.Expr.DecSmall,
small: @TypeOf(@as(can.CIR.Expr, undefined).e_dec_small),
) Error!StackValue {
const rt_var = expected_rt_var orelse blk: {
const ct_var = can.ModuleEnv.varFrom(expr_idx);
@ -7660,7 +7814,7 @@ pub const Interpreter = struct {
/// Evaluate a string segment literal (e_str_segment)
fn evalStrSegment(
self: *Interpreter,
seg: can.CIR.Expr.StrSegment,
seg: @TypeOf(@as(can.CIR.Expr, undefined).e_str_segment),
roc_ops: *RocOps,
) Error!StackValue {
const content = self.env.getString(seg.literal);
@ -7719,7 +7873,7 @@ pub const Interpreter = struct {
self: *Interpreter,
expr_idx: can.CIR.Expr.Idx,
expected_rt_var: ?types.Var,
zero: can.CIR.Expr.ZeroArgumentTag,
zero: @TypeOf(@as(can.CIR.Expr, undefined).e_zero_argument_tag),
roc_ops: *RocOps,
) Error!StackValue {
const rt_var = expected_rt_var orelse blk: {
@ -7804,7 +7958,7 @@ pub const Interpreter = struct {
/// lazy evaluation of top-level definitions.
fn evalLookupLocal(
self: *Interpreter,
lookup: can.CIR.Expr.LookupLocal,
lookup: @TypeOf(@as(can.CIR.Expr, undefined).e_lookup_local),
roc_ops: *RocOps,
) Error!StackValue {
// Search bindings in reverse
@ -7890,7 +8044,7 @@ pub const Interpreter = struct {
/// NOTE: This still uses recursive evaluation - will be fully converted in a later PR
fn evalLookupExternal(
self: *Interpreter,
lookup: can.CIR.Expr.LookupExternal,
lookup: @TypeOf(@as(can.CIR.Expr, undefined).e_lookup_external),
expected_rt_var: ?types.Var,
roc_ops: *RocOps,
) Error!StackValue {
@ -8018,6 +8172,7 @@ pub const Interpreter = struct {
remaining_stmts: []const can.CIR.Statement.Idx,
final_expr: can.CIR.Expr.Idx,
bindings_start: usize,
roc_ops: *RocOps,
) Error!void {
switch (stmt) {
.s_decl => |d| {
@ -8085,8 +8240,205 @@ pub const Interpreter = struct {
.expected_rt_var = null,
} });
},
.s_crash => |c| {
const msg = self.env.getString(c.msg);
self.triggerCrash(msg, false, roc_ops);
return error.Crash;
},
.s_expect => |expect_stmt| {
// Fall back to recursive for now
const bool_rt_var = try self.getCanonicalBoolRuntimeVar();
const cond_val = try self.evalExprMinimal(expect_stmt.body, roc_ops, bool_rt_var);
const is_true = boolValueEquals(true, cond_val);
if (!is_true) {
self.handleExpectFailure(expect_stmt.body, roc_ops);
return error.Crash;
}
// Continue with remaining statements
if (remaining_stmts.len == 0) {
try work_stack.push(.{ .eval_expr = .{
.expr_idx = final_expr,
.expected_rt_var = null,
} });
} else {
const next_stmt = self.env.store.getStatement(remaining_stmts[0]);
try self.scheduleNextStatement(work_stack, next_stmt, remaining_stmts[1..], final_expr, bindings_start, roc_ops);
}
},
.s_reassign => |r| {
// Schedule: evaluate expression, then reassign
// For now use recursive fallback for the expression
const new_val = try self.evalExprMinimal(r.expr, roc_ops, null);
// Search through all bindings and reassign
var j: usize = self.bindings.items.len;
while (j > 0) {
j -= 1;
if (self.bindings.items[j].pattern_idx == r.pattern_idx) {
self.bindings.items[j].value.decref(&self.runtime_layout_store, roc_ops);
self.bindings.items[j].value = new_val;
break;
}
}
// Continue with remaining statements
if (remaining_stmts.len == 0) {
try work_stack.push(.{ .eval_expr = .{
.expr_idx = final_expr,
.expected_rt_var = null,
} });
} else {
const next_stmt = self.env.store.getStatement(remaining_stmts[0]);
try self.scheduleNextStatement(work_stack, next_stmt, remaining_stmts[1..], final_expr, bindings_start, roc_ops);
}
},
.s_dbg => |dbg_stmt| {
// Fall back to recursive for now
const inner_ct_var = can.ModuleEnv.varFrom(dbg_stmt.expr);
const inner_rt_var = try self.translateTypeVar(self.env, inner_ct_var);
const value = try self.evalExprMinimal(dbg_stmt.expr, roc_ops, inner_rt_var);
defer value.decref(&self.runtime_layout_store, roc_ops);
const rendered = try self.renderValueRocWithType(value, inner_rt_var);
defer self.allocator.free(rendered);
roc_ops.dbg(rendered);
// Continue with remaining statements
if (remaining_stmts.len == 0) {
try work_stack.push(.{ .eval_expr = .{
.expr_idx = final_expr,
.expected_rt_var = null,
} });
} else {
const next_stmt = self.env.store.getStatement(remaining_stmts[0]);
try self.scheduleNextStatement(work_stack, next_stmt, remaining_stmts[1..], final_expr, bindings_start, roc_ops);
}
},
.s_return => |ret| {
// Early return: evaluate expression, store value, signal return
const expr_ct_var = can.ModuleEnv.varFrom(ret.expr);
const expr_rt_var = try self.translateTypeVar(self.env, expr_ct_var);
const return_value = try self.evalExprMinimal(ret.expr, roc_ops, expr_rt_var);
// Store the return value for the caller to consume
self.early_return_value = return_value;
return error.EarlyReturn;
},
.s_for => |for_stmt| {
// Fall back to recursive evaluation for for loops (complex control flow)
// Evaluate the list expression
const expr_ct_var = can.ModuleEnv.varFrom(for_stmt.expr);
const expr_rt_var = try self.translateTypeVar(self.env, expr_ct_var);
const list_value = try self.evalExprMinimal(for_stmt.expr, roc_ops, expr_rt_var);
defer list_value.decref(&self.runtime_layout_store, roc_ops);
// Get the list layout
if (list_value.layout.tag != .list) {
return error.TypeMismatch;
}
const elem_layout_idx = list_value.layout.data.list;
const elem_layout = self.runtime_layout_store.getLayout(elem_layout_idx);
const elem_size: usize = @intCast(self.runtime_layout_store.layoutSize(elem_layout));
// Get the RocList header
const list_header: *const RocList = @ptrCast(@alignCast(list_value.ptr.?));
const list_len = list_header.len();
// Get the element type for binding
const patt_ct_var = can.ModuleEnv.varFrom(for_stmt.patt);
const patt_rt_var = try self.translateTypeVar(self.env, patt_ct_var);
// Iterate over each element
var i: usize = 0;
while (i < list_len) : (i += 1) {
// Get pointer to element
const elem_ptr = if (list_header.bytes) |buffer|
buffer + i * elem_size
else
return error.TypeMismatch;
// Create a StackValue from the element
var elem_value = StackValue{
.ptr = elem_ptr,
.layout = elem_layout,
.is_initialized = true,
};
// Increment refcount since we're creating a new reference
elem_value.incref();
// Bind the pattern to the element value
var temp_binds = try std.array_list.AlignedManaged(Binding, null).initCapacity(self.allocator, 4);
defer {
self.trimBindingList(&temp_binds, 0, roc_ops);
temp_binds.deinit();
}
if (!try self.patternMatchesBind(for_stmt.patt, elem_value, patt_rt_var, roc_ops, &temp_binds, @enumFromInt(0))) {
elem_value.decref(&self.runtime_layout_store, roc_ops);
return error.TypeMismatch;
}
// Add bindings to the environment
const loop_bindings_start = self.bindings.items.len;
for (temp_binds.items) |binding| {
try self.bindings.append(binding);
}
// Evaluate the body using recursive interpreter
const body_result = self.evalExprMinimal(for_stmt.body, roc_ops, null) catch |err| {
// Clean up before propagating error
self.trimBindingList(&self.bindings, loop_bindings_start, roc_ops);
elem_value.decref(&self.runtime_layout_store, roc_ops);
return err;
};
body_result.decref(&self.runtime_layout_store, roc_ops);
// Clean up bindings for this iteration
self.trimBindingList(&self.bindings, loop_bindings_start, roc_ops);
// Decrement the element reference (it was incremented above)
elem_value.decref(&self.runtime_layout_store, roc_ops);
}
// Continue with remaining statements
if (remaining_stmts.len == 0) {
try work_stack.push(.{ .eval_expr = .{
.expr_idx = final_expr,
.expected_rt_var = null,
} });
} else {
const next_stmt = self.env.store.getStatement(remaining_stmts[0]);
try self.scheduleNextStatement(work_stack, next_stmt, remaining_stmts[1..], final_expr, bindings_start, roc_ops);
}
},
.s_while => |while_stmt| {
// Fall back to recursive evaluation for while loops
while (true) {
const cond_ct_var = can.ModuleEnv.varFrom(while_stmt.cond);
const cond_rt_var = try self.translateTypeVar(self.env, cond_ct_var);
const cond_value = try self.evalExprMinimal(while_stmt.cond, roc_ops, cond_rt_var);
const cond_is_true = boolValueEquals(true, cond_value);
if (!cond_is_true) {
break;
}
const body_result = self.evalExprMinimal(while_stmt.body, roc_ops, null) catch |err| {
return err;
};
body_result.decref(&self.runtime_layout_store, roc_ops);
}
// Continue with remaining statements
if (remaining_stmts.len == 0) {
try work_stack.push(.{ .eval_expr = .{
.expr_idx = final_expr,
.expected_rt_var = null,
} });
} else {
const next_stmt = self.env.store.getStatement(remaining_stmts[0]);
try self.scheduleNextStatement(work_stack, next_stmt, remaining_stmts[1..], final_expr, bindings_start, roc_ops);
}
},
else => {
// Other statement types will be implemented in PR 7
// Other statement types
@panic("Stack-safe interpreter: statement type not yet implemented");
},
}
@ -8204,7 +8556,7 @@ pub const Interpreter = struct {
} else {
// Process next statement
const next_stmt = self.env.store.getStatement(bc.remaining_stmts[0]);
try self.scheduleNextStatement(work_stack, next_stmt, bc.remaining_stmts[1..], bc.final_expr, bc.bindings_start);
try self.scheduleNextStatement(work_stack, next_stmt, bc.remaining_stmts[1..], bc.final_expr, bc.bindings_start, roc_ops);
}
return true;
},
@ -8243,7 +8595,307 @@ pub const Interpreter = struct {
} else {
// Process next statement
const next_stmt = self.env.store.getStatement(bd.remaining_stmts[0]);
try self.scheduleNextStatement(work_stack, next_stmt, bd.remaining_stmts[1..], bd.final_expr, bd.bindings_start);
try self.scheduleNextStatement(work_stack, next_stmt, bd.remaining_stmts[1..], bd.final_expr, bd.bindings_start, roc_ops);
}
return true;
},
.tuple_collect => |tc| {
// Tuple collection works by evaluating elements one at a time
// and tracking how many we've collected
if (tc.remaining_elems.len > 0) {
// More elements to evaluate - schedule next one
try work_stack.push(.{ .apply_continuation = .{ .tuple_collect = .{
.collected_count = tc.collected_count + 1,
.remaining_elems = tc.remaining_elems[1..],
} } });
try work_stack.push(.{ .eval_expr = .{
.expr_idx = tc.remaining_elems[0],
.expected_rt_var = null,
} });
} else {
// All elements evaluated - finalize the tuple
// Pop all collected values from the value stack
const total_count = tc.collected_count;
if (total_count == 0) {
// Empty tuple (shouldn't happen as it's handled directly)
const tuple_layout_idx = try self.runtime_layout_store.putTuple(&[0]Layout{});
const tuple_layout = self.runtime_layout_store.getLayout(tuple_layout_idx);
const tuple_val = try self.pushRaw(tuple_layout, 0);
try value_stack.push(tuple_val);
} else {
// Gather layouts and values
var elem_layouts = try self.allocator.alloc(Layout, total_count);
defer self.allocator.free(elem_layouts);
// Values are in reverse order on stack (first element pushed first, so it's at the bottom)
// We need to pop them and store in correct order
var values = try self.allocator.alloc(StackValue, total_count);
defer self.allocator.free(values);
// Pop values in reverse order (last evaluated is on top)
var i: usize = total_count;
while (i > 0) {
i -= 1;
values[i] = value_stack.pop() orelse return error.Crash;
elem_layouts[i] = values[i].layout;
}
// Create tuple layout
const tuple_layout_idx = try self.runtime_layout_store.putTuple(elem_layouts);
const tuple_layout = self.runtime_layout_store.getLayout(tuple_layout_idx);
var dest = try self.pushRaw(tuple_layout, 0);
var accessor = try dest.asTuple(&self.runtime_layout_store);
if (total_count != accessor.getElementCount()) return error.TypeMismatch;
// Set all elements
for (0..total_count) |idx| {
try accessor.setElement(idx, values[idx], roc_ops);
}
// Decref temporary values after they've been copied into the tuple
for (values) |val| {
val.decref(&self.runtime_layout_store, roc_ops);
}
try value_stack.push(dest);
}
}
return true;
},
.list_collect => |lc| {
// List collection works by evaluating elements one at a time
// and tracking how many we've collected
if (lc.remaining_elems.len > 0) {
// More elements to evaluate - schedule next one
try work_stack.push(.{ .apply_continuation = .{ .list_collect = .{
.collected_count = lc.collected_count + 1,
.remaining_elems = lc.remaining_elems[1..],
.elem_rt_var = lc.elem_rt_var,
.list_rt_var = lc.list_rt_var,
} } });
try work_stack.push(.{ .eval_expr = .{
.expr_idx = lc.remaining_elems[0],
.expected_rt_var = lc.elem_rt_var,
} });
} else {
// All elements evaluated - finalize the list
const total_count = lc.collected_count;
if (total_count == 0) {
// Empty list (shouldn't happen as it's handled directly)
const list_layout = try self.getRuntimeLayout(lc.list_rt_var);
const dest = try self.pushRaw(list_layout, 0);
if (dest.ptr != null) {
const header: *RocList = @ptrCast(@alignCast(dest.ptr.?));
header.* = RocList.empty();
}
try value_stack.push(dest);
} else {
// Pop all collected values from the value stack
var values = try self.allocator.alloc(StackValue, total_count);
defer self.allocator.free(values);
// Pop values in reverse order (last evaluated is on top)
var i: usize = total_count;
while (i > 0) {
i -= 1;
values[i] = value_stack.pop() orelse return error.Crash;
}
// Use the actual layout from the first evaluated element
const actual_elem_layout = values[0].layout;
// Create the list layout with the correct element layout
const correct_elem_idx = try self.runtime_layout_store.insertLayout(actual_elem_layout);
const actual_list_layout = Layout{ .tag = .list, .data = .{ .list = correct_elem_idx } };
const dest = try self.pushRaw(actual_list_layout, 0);
if (dest.ptr == null) {
// Decref all values before returning
for (values) |val| {
val.decref(&self.runtime_layout_store, roc_ops);
}
try value_stack.push(dest);
return true;
}
const header: *RocList = @ptrCast(@alignCast(dest.ptr.?));
const elem_alignment = actual_elem_layout.alignment(self.runtime_layout_store.targetUsize()).toByteUnits();
const elem_alignment_u32: u32 = @intCast(elem_alignment);
const elem_size: usize = @intCast(self.runtime_layout_store.layoutSize(actual_elem_layout));
const elements_refcounted = actual_elem_layout.isRefcounted();
var runtime_list = RocList.allocateExact(
elem_alignment_u32,
total_count,
elem_size,
elements_refcounted,
roc_ops,
);
if (elem_size > 0) {
if (runtime_list.bytes) |buffer| {
for (values, 0..) |val, idx| {
const dest_ptr = buffer + idx * elem_size;
try val.copyToPtr(&self.runtime_layout_store, dest_ptr, roc_ops);
}
}
}
markListElementCount(&runtime_list, elements_refcounted);
header.* = runtime_list;
// Decref temporary values after they've been copied into the list
for (values) |val| {
val.decref(&self.runtime_layout_store, roc_ops);
}
try value_stack.push(dest);
}
}
return true;
},
.record_collect => |rc| {
// Record collection: evaluate extension (if any), then fields in order
if (rc.remaining_fields.len > 0) {
// More fields to evaluate - schedule next one
const next_field_idx = rc.remaining_fields[0];
const f = self.env.store.getRecordField(next_field_idx);
const field_ct_var = can.ModuleEnv.varFrom(f.value);
const field_rt_var = try self.translateTypeVar(self.env, field_ct_var);
try work_stack.push(.{ .apply_continuation = .{ .record_collect = .{
.collected_count = rc.collected_count + 1,
.remaining_fields = rc.remaining_fields[1..],
.rt_var = rc.rt_var,
.expr_idx = rc.expr_idx,
.has_extension = rc.has_extension,
.all_fields = rc.all_fields,
} } });
try work_stack.push(.{ .eval_expr = .{
.expr_idx = f.value,
.expected_rt_var = field_rt_var,
} });
} else {
// All values collected - finalize the record
const total_field_values = rc.collected_count;
// Build layout info from collected values
var union_names = std.array_list.AlignedManaged(base_pkg.Ident.Idx, null).init(self.allocator);
defer union_names.deinit();
var union_layouts = std.array_list.AlignedManaged(layout.Layout, null).init(self.allocator);
defer union_layouts.deinit();
var union_indices = std.AutoHashMap(u32, usize).init(self.allocator);
defer union_indices.deinit();
// Pop field values from stack (in reverse order since last evaluated is on top)
var field_values = try self.allocator.alloc(StackValue, total_field_values);
defer self.allocator.free(field_values);
var i: usize = total_field_values;
while (i > 0) {
i -= 1;
field_values[i] = value_stack.pop() orelse return error.Crash;
}
// Handle base record if extension exists
var base_value_opt: ?StackValue = null;
if (rc.has_extension) {
base_value_opt = value_stack.pop() orelse return error.Crash;
const base_value = base_value_opt.?;
if (base_value.layout.tag != .record) {
base_value.decref(&self.runtime_layout_store, roc_ops);
for (field_values) |fv| fv.decref(&self.runtime_layout_store, roc_ops);
return error.TypeMismatch;
}
var base_accessor = try base_value.asRecord(&self.runtime_layout_store);
// Add base record fields to union
var idx: usize = 0;
while (idx < base_accessor.getFieldCount()) : (idx += 1) {
const info = base_accessor.field_layouts.get(idx);
const field_layout = self.runtime_layout_store.getLayout(info.layout);
const key: u32 = @bitCast(info.name);
if (union_indices.get(key)) |idx_ptr| {
union_layouts.items[idx_ptr] = field_layout;
union_names.items[idx_ptr] = info.name;
} else {
try union_layouts.append(field_layout);
try union_names.append(info.name);
try union_indices.put(key, union_layouts.items.len - 1);
}
}
}
// Add explicit field layouts to union
for (rc.all_fields, 0..) |field_idx_enum, idx| {
const f = self.env.store.getRecordField(field_idx_enum);
const field_layout = field_values[idx].layout;
const key: u32 = @bitCast(f.name);
if (union_indices.get(key)) |idx_ptr| {
union_layouts.items[idx_ptr] = field_layout;
union_names.items[idx_ptr] = f.name;
} else {
try union_layouts.append(field_layout);
try union_names.append(f.name);
try union_indices.put(key, union_layouts.items.len - 1);
}
}
// Create record layout
const record_layout_idx = try self.runtime_layout_store.putRecord(self.env, union_layouts.items, union_names.items);
const rec_layout = self.runtime_layout_store.getLayout(record_layout_idx);
// Cache the layout for this var
const resolved_rt = self.runtime_types.resolveVar(rc.rt_var);
const root_idx: usize = @intFromEnum(resolved_rt.var_);
try self.ensureVarLayoutCapacity(root_idx + 1);
self.var_to_layout_slot.items[root_idx] = @intFromEnum(record_layout_idx) + 1;
var dest = try self.pushRaw(rec_layout, 0);
var accessor = try dest.asRecord(&self.runtime_layout_store);
// Copy base record fields first
if (base_value_opt) |base_value| {
var base_accessor = try base_value.asRecord(&self.runtime_layout_store);
var idx: usize = 0;
while (idx < base_accessor.getFieldCount()) : (idx += 1) {
const info = base_accessor.field_layouts.get(idx);
const dest_field_idx = accessor.findFieldIndex(info.name) orelse return error.TypeMismatch;
const base_field_value = try base_accessor.getFieldByIndex(idx);
try accessor.setFieldByIndex(dest_field_idx, base_field_value, roc_ops);
}
}
// Set explicit field values (overwriting base values if needed)
for (rc.all_fields, 0..) |field_idx_enum, explicit_index| {
const f = self.env.store.getRecordField(field_idx_enum);
const dest_field_idx = accessor.findFieldIndex(f.name) orelse return error.TypeMismatch;
const val = field_values[explicit_index];
// If overwriting a base field, decref the existing value
if (base_value_opt) |base_value| {
var base_accessor = try base_value.asRecord(&self.runtime_layout_store);
if (base_accessor.findFieldIndex(f.name) != null) {
const existing = try accessor.getFieldByIndex(dest_field_idx);
existing.decref(&self.runtime_layout_store, roc_ops);
}
}
try accessor.setFieldByIndex(dest_field_idx, val, roc_ops);
}
// Decref base value and field values after they've been copied
if (base_value_opt) |base_value| {
base_value.decref(&self.runtime_layout_store, roc_ops);
}
for (field_values) |val| {
val.decref(&self.runtime_layout_store, roc_ops);
}
try value_stack.push(dest);
}
return true;
},