Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
e9b0742
Re-enable low_level_interp_test
rtfeldman Nov 19, 2025
0cf59fd
Fix low_level_interp_test
rtfeldman Nov 19, 2025
4f6517a
Disable some memory leak checks for now
rtfeldman Nov 19, 2025
a33cc0e
Fix refcounting bug
rtfeldman Nov 19, 2025
3156742
Add more refcounting tests
rtfeldman Nov 19, 2025
cb8525c
Add some list refcounting tests
rtfeldman Nov 19, 2025
89176f5
Add some list refcounting tests
rtfeldman Nov 19, 2025
32fa160
Merge remote-tracking branch 'origin/remove-box-primitive' into fix-r…
rtfeldman Nov 19, 2025
b5c2759
Fix test
rtfeldman Nov 19, 2025
bb71fe5
reproduce interpreter bug
rtfeldman Nov 19, 2025
9e068a0
wip
rtfeldman Nov 19, 2025
3656bbe
Revert "wip"
rtfeldman Nov 19, 2025
c799670
Revert "Disable some memory leak checks for now"
rtfeldman Nov 19, 2025
79a037c
Merge remote-tracking branch 'origin/fix-refcounting' into fix-low-le…
rtfeldman Nov 19, 2025
b78c3d1
Fix a canonicalization bug
rtfeldman Nov 19, 2025
57ef075
Use copyForward instead of memcpy
rtfeldman Nov 19, 2025
9d134e4
Fix more memory issues
rtfeldman Nov 19, 2025
8750552
wip
rtfeldman Nov 20, 2025
de0f591
Disable let-generalization for now
rtfeldman Nov 20, 2025
2ea8cf0
Restore let-polymorphism for numbers and lambdas
rtfeldman Nov 20, 2025
a28786c
More let-generalization fixes
rtfeldman Nov 20, 2025
508d540
Clean up some debug stuff
rtfeldman Nov 20, 2025
816b1f6
Remove more std.debug.print stuff
rtfeldman Nov 20, 2025
5e3384e
Restore ModuleEnv from origin/main
rtfeldman Nov 20, 2025
966cc8b
Fail `zig build test` if src/ uses std.debug.print
rtfeldman Nov 20, 2025
6d9a766
Delete obsolete test
rtfeldman Nov 20, 2025
5b40248
Consolidate regression tests
rtfeldman Nov 20, 2025
cfbb310
Use tuples, not records, for tag unions at runtime
rtfeldman Nov 20, 2025
ee51bcf
wip
rtfeldman Nov 20, 2025
9bf2404
Merge remote-tracking branch 'origin/fix-layout-bug' into fix-low-lev…
rtfeldman Nov 20, 2025
31705b2
Merge origin/main
rtfeldman Nov 22, 2025
95767cf
Fix some more things
rtfeldman Nov 23, 2025
a283e0e
Fix tag union rendering mismatch
rtfeldman Nov 23, 2025
5dcc849
Remove some debug prints
rtfeldman Nov 23, 2025
c42e2b4
Fix non-exhaustive match crashes
rtfeldman Nov 23, 2025
489166a
Fix number literal errors
rtfeldman Nov 23, 2025
b62a367
Some more fixes
rtfeldman Nov 23, 2025
0f079fa
Partial fix
rtfeldman Nov 23, 2025
d8b0f7b
Fix more tests
rtfeldman Nov 23, 2025
28f40f7
Fix memory leaks
rtfeldman Nov 23, 2025
abdfe27
Fix multi-arg tuples
rtfeldman Nov 23, 2025
c4b8bf4
Merge remote-tracking branch 'origin/fix-leaks' into fix-low-level-in…
rtfeldman Nov 23, 2025
75f748b
Fix some list layout bugs
rtfeldman Nov 23, 2025
8326d7e
Fix multi-arg tags
rtfeldman Nov 23, 2025
2c02975
Fix remaining leaks
rtfeldman Nov 23, 2025
9933217
Merge origin/main
rtfeldman Nov 23, 2025
651860f
Fix remaining tests
rtfeldman Nov 23, 2025
c3a9c73
Merge remote-tracking branch 'origin/main' into fix-low-level-interp
rtfeldman Nov 23, 2025
546a7c6
Fix some eval issues
rtfeldman Nov 23, 2025
02cc989
Fix comptime evaluator cross-compilation
rtfeldman Nov 23, 2025
507b22b
Use arena allocator for comptime evaluator
rtfeldman Nov 23, 2025
0718ed5
Use @memmove over deprecated copyForwards
rtfeldman Nov 23, 2025
629d7f5
Drop a no-op
rtfeldman Nov 23, 2025
3ee470e
Clean up some comments
rtfeldman Nov 23, 2025
949da34
Remove some unused debugging things
rtfeldman Nov 23, 2025
e2ef92a
Rename list_refcount_MINIMAL to not be yelling
rtfeldman Nov 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/builtins/list.zig
Original file line number Diff line number Diff line change
Expand Up @@ -325,11 +325,13 @@ pub const RocList = extern struct {
if (self.isUnique() and !self.isSeamlessSlice()) {
const capacity = self.capacity_or_alloc_ptr;
if (capacity >= new_length) {
return RocList{ .bytes = self.bytes, .length = new_length, .capacity_or_alloc_ptr = capacity };
const result = RocList{ .bytes = self.bytes, .length = new_length, .capacity_or_alloc_ptr = capacity };
return result;
} else {
const new_capacity = utils.calculateCapacity(capacity, new_length, element_width);
const new_source = utils.unsafeReallocate(source_ptr, alignment, capacity, new_capacity, element_width, elements_refcounted);
return RocList{ .bytes = new_source, .length = new_length, .capacity_or_alloc_ptr = new_capacity };
const new_source = utils.unsafeReallocate(source_ptr, alignment, capacity, new_capacity, element_width, elements_refcounted, roc_ops);
const result = RocList{ .bytes = new_source, .length = new_length, .capacity_or_alloc_ptr = new_capacity };
return result;
}
}
return self.reallocateFresh(alignment, new_length, element_width, elements_refcounted, inc_context, inc, roc_ops);
Expand Down Expand Up @@ -1124,7 +1126,11 @@ pub fn listConcat(
// These must exist, otherwise, the lists would have been empty.
const source_a = resized_list_a.bytes orelse unreachable;
const source_b = list_b.bytes orelse unreachable;
@memcpy(source_a[(list_a.len() * element_width)..(total_length * element_width)], source_b[0..(list_b.len() * element_width)]);

// Use @memmove instead of @memcpy to handle potential aliasing
const dest_slice = source_a[(list_a.len() * element_width)..(total_length * element_width)];
const src_slice = source_b[0..(list_b.len() * element_width)];
@memmove(dest_slice, src_slice);

// Increment refcount of all cloned elements.
if (elements_refcounted) {
Expand Down
1 change: 1 addition & 0 deletions src/builtins/str.zig
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ pub const RocStr = extern struct {
new_capacity,
element_width,
false,
roc_ops,
);

return RocStr{ .bytes = new_source, .length = new_length, .capacity_or_alloc_ptr = new_capacity };
Expand Down
84 changes: 40 additions & 44 deletions src/builtins/utils.zig
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ const RocDbg = @import("host_abi.zig").RocDbg;
const RocExpectFailed = @import("host_abi.zig").RocExpectFailed;
const RocCrashed = @import("host_abi.zig").RocCrashed;

const DEBUG_INCDEC = false;
const DEBUG_TESTING_ALLOC = false;
const DEBUG_ALLOC = false;

/// Tracks allocations for testing purposes with C ABI compatibility. Uses a single global testing allocator to track allocations. If we need multiple independent allocators we will need to modify this and use comptime.
pub const TestEnv = struct {
Expand Down Expand Up @@ -129,21 +127,50 @@ pub const TestEnv = struct {
}

fn rocReallocFn(roc_realloc: *RocRealloc, env: *anyopaque) callconv(.c) void {
_ = env;
_ = roc_realloc;
@panic("Test realloc not implemented yet");
const self: *TestEnv = @ptrCast(@alignCast(env));

// Look up the old allocation
if (self.allocation_map.fetchRemove(roc_realloc.answer)) |entry| {
const old_bytes: [*]u8 = @ptrCast(@alignCast(roc_realloc.answer));
const old_slice = old_bytes[0..entry.value.size];

// Reallocate with the same alignment
const new_ptr = switch (entry.value.alignment) {
1 => self.allocator.realloc(old_slice, roc_realloc.new_length),
2 => self.allocator.realloc(@as([]align(2) u8, @alignCast(old_slice)), roc_realloc.new_length),
4 => self.allocator.realloc(@as([]align(4) u8, @alignCast(old_slice)), roc_realloc.new_length),
8 => self.allocator.realloc(@as([]align(8) u8, @alignCast(old_slice)), roc_realloc.new_length),
16 => self.allocator.realloc(@as([]align(16) u8, @alignCast(old_slice)), roc_realloc.new_length),
else => @panic("Unsupported alignment in test reallocator"),
} catch {
@panic("Test reallocation failed");
};

const result: *anyopaque = @ptrCast(new_ptr.ptr);

// Update the allocation map with the new pointer and size
self.allocation_map.put(result, AllocationInfo{
.size = roc_realloc.new_length,
.alignment = entry.value.alignment,
}) catch {
self.allocator.free(new_ptr);
@panic("Failed to track test reallocation");
};

roc_realloc.answer = result;
} else {
@panic("Test realloc: pointer not found in allocation map");
}
}

fn rocDbgFn(roc_dbg: *const RocDbg, env: *anyopaque) callconv(.c) void {
_ = env;
const message = roc_dbg.utf8_bytes[0..roc_dbg.len];
std.debug.print("DBG: {s}\n", .{message});
_ = roc_dbg;
}

fn rocExpectFailedFn(roc_expect: *const RocExpectFailed, env: *anyopaque) callconv(.c) void {
_ = env;
const message = @as([*]u8, @ptrCast(roc_expect.utf8_bytes))[0..roc_expect.len];
std.debug.print("EXPECT FAILED: {s}\n", .{message});
_ = roc_expect;
}

fn rocCrashedFn(roc_crashed: *const RocCrashed, env: *anyopaque) callconv(.c) noreturn {
Expand Down Expand Up @@ -212,24 +239,13 @@ const RC_TYPE: Refcount = .atomic;
pub fn increfRcPtrC(ptr_to_refcount: *isize, amount: isize) callconv(.c) void {
if (RC_TYPE == .none) return;

if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) {
std.debug.print("| increment {*}: ", .{ptr_to_refcount});
}

// Ensure that the refcount is not whole program lifetime.
const refcount: isize = ptr_to_refcount.*;
if (!rcConstant(refcount)) {
// Note: we assume that a refcount will never overflow.
// As such, we do not need to cap incrementing.
switch (RC_TYPE) {
.normal => {
if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) {
const old = @as(usize, @bitCast(refcount));
const new = old + @as(usize, @intCast(amount));

std.debug.print("{} + {} = {}!\n", .{ old, amount, new });
}

ptr_to_refcount.* = refcount +% amount;
},
.atomic => {
Expand Down Expand Up @@ -388,10 +404,6 @@ inline fn free_ptr_to_refcount(

// NOTE: we don't even check whether the refcount is "infinity" here!
roc_ops.roc_dealloc(&roc_dealloc_args, roc_ops.env);

if (DEBUG_ALLOC and builtin.target.cpu.arch != .wasm32) {
std.debug.print("💀 freed {*}\n", .{allocation_ptr});
}
}

inline fn decref_ptr_to_refcount(
Expand All @@ -402,10 +414,6 @@ inline fn decref_ptr_to_refcount(
) void {
if (RC_TYPE == .none) return;

if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) {
std.debug.print("| decrement {*}: ", .{refcount_ptr});
}

// Due to RC alignment tmust take into account pointer size.
const ptr_width = @sizeOf(usize);
const alignment = @max(ptr_width, element_alignment);
Expand All @@ -415,13 +423,6 @@ inline fn decref_ptr_to_refcount(
if (!rcConstant(refcount)) {
switch (RC_TYPE) {
.normal => {
if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) {
const old = @as(usize, @bitCast(refcount));
const new = @as(usize, @bitCast(refcount_ptr[0] -% 1));

std.debug.print("{} - 1 = {}!\n", .{ old, new });
}

refcount_ptr[0] = refcount -% 1;
if (refcount == 1) {
free_ptr_to_refcount(refcount_ptr, alignment, elements_refcounted, roc_ops);
Expand Down Expand Up @@ -454,10 +455,6 @@ pub fn isUnique(

const refcount = (isizes - 1)[0];

if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) {
std.debug.print("| is unique {*}\n", .{isizes - 1});
}

return rcUnique(refcount);
}

Expand Down Expand Up @@ -583,10 +580,6 @@ pub fn allocateWithRefcount(

const new_bytes = @as([*]u8, @ptrCast(roc_alloc_args.answer));

if (DEBUG_ALLOC and builtin.target.cpu.arch != .wasm32) {
std.debug.print("+ allocated {*} ({} bytes with alignment {})\n", .{ new_bytes, data_bytes, alignment });
}

const data_ptr = new_bytes + extra_bytes;
const refcount_ptr = @as([*]usize, @ptrCast(@as([*]align(ptr_width) u8, @alignCast(data_ptr)) - ptr_width));
refcount_ptr[0] = if (RC_TYPE == .none) REFCOUNT_STATIC_DATA else 1;
Expand All @@ -610,6 +603,7 @@ pub fn unsafeReallocate(
new_length: usize,
element_width: usize,
elements_refcounted: bool,
roc_ops: *RocOps,
) [*]u8 {
const ptr_width: usize = @sizeOf(usize);
const required_space: usize = if (elements_refcounted) (2 * ptr_width) else ptr_width;
Expand All @@ -624,12 +618,14 @@ pub fn unsafeReallocate(

const old_allocation = source_ptr - extra_bytes;

const roc_realloc_args = RocRealloc{
var roc_realloc_args = RocRealloc{
.alignment = alignment,
.new_length = new_width,
.answer = old_allocation,
};

roc_ops.roc_realloc(&roc_realloc_args, roc_ops.env);

const new_source = @as([*]u8, @ptrCast(roc_realloc_args.answer)) + extra_bytes;
return new_source;
}
Expand Down
49 changes: 38 additions & 11 deletions src/canonicalize/Can.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4664,7 +4664,6 @@ fn canonicalizeExprOrMalformed(
// Canonicalize a tag expr
fn canonicalizeTagExpr(self: *Self, e: AST.TagExpr, mb_args: ?AST.Expr.Span, region: base.Region) std.mem.Allocator.Error!?CanonicalizedExpr {
const tag_name = self.parse_ir.tokens.resolveIdentifier(e.token) orelse @panic("tag token is not an ident");

var args_span = Expr.Span{ .span = DataSpan.empty() };

const free_vars_start = self.scratch_free_vars.top();
Expand Down Expand Up @@ -8226,16 +8225,42 @@ pub fn canonicalizeBlockDecl(self: *Self, d: AST.Statement.Decl, mb_last_anno: ?
// Canonicalize the decl expr
const expr = try self.canonicalizeExprOrMalformed(d.body);

// Create a declaration statement
const stmt_idx = try self.env.addStatement(Statement{ .s_decl = .{
.pattern = pattern_idx,
.expr = expr.idx,
.anno = mb_validated_anno,
} }, region);
// Determine if we should generalize based on RHS
const should_generalize = self.shouldGeneralizeBinding(expr.idx);

// Create a declaration statement (generalized or not)
const stmt_idx = if (should_generalize)
try self.env.addStatement(Statement{ .s_decl_gen = .{
.pattern = pattern_idx,
.expr = expr.idx,
.anno = mb_validated_anno,
} }, region)
else
try self.env.addStatement(Statement{ .s_decl = .{
.pattern = pattern_idx,
.expr = expr.idx,
.anno = mb_validated_anno,
} }, region);

return CanonicalizedStatement{ .idx = stmt_idx, .free_vars = expr.free_vars };
}

/// Determines whether a let binding should be generalized based on its RHS expression.
/// According to Roc's value restriction, only lambdas and number literals should be generalized.
fn shouldGeneralizeBinding(self: *Self, expr_idx: Expr.Idx) bool {
const expr = self.env.store.getExpr(expr_idx);
return switch (expr) {
// Lambdas should be generalized (both closures and pure lambdas)
.e_closure, .e_lambda => true,

// Number literals should be generalized
.e_num, .e_frac_f32, .e_frac_f64, .e_dec, .e_dec_small => true,

// Everything else should NOT be generalized
else => false,
};
}

// A canonicalized statement
const CanonicalizedStatement = struct {
idx: Statement.Idx,
Expand Down Expand Up @@ -9492,10 +9517,12 @@ fn tryModuleQualifiedLookup(self: *Self, field_access: AST.BinOp) std.mem.Alloca
const import_idx = self.scopeLookupImportedModule(module_text) orelse blk: {
// Module not in import scope - check if it's an auto-imported module in module_envs
if (self.module_envs) |envs_map| {
if (envs_map.get(module_name)) |_| {
// This is an auto-imported module (like Bool, Try, etc.)
// Create an import for it dynamically
break :blk try self.getOrCreateAutoImport(module_text);
if (envs_map.get(module_name)) |auto_imported_type| {
// This is an auto-imported module (like Bool, Try, Str, List, etc.)
// Use the ACTUAL module name from the environment, not the alias
// This ensures all auto-imported types from the same module share the same Import.Idx
const actual_module_name = auto_imported_type.env.module_name;
break :blk try self.getOrCreateAutoImport(actual_module_name);
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/canonicalize/DependencyGraph.zig
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ fn collectExprDependencies(
.s_decl => |decl| {
try collectExprDependencies(cir, decl.expr, dependencies, allocator);
},
.s_decl_gen => |decl| {
try collectExprDependencies(cir, decl.expr, dependencies, allocator);
},
.s_var => |var_stmt| {
try collectExprDependencies(cir, var_stmt.expr, dependencies, allocator);
},
Expand Down
1 change: 1 addition & 0 deletions src/canonicalize/Node.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub const Idx = List.Idx;
pub const Tag = enum {
// Statements
statement_decl,
statement_decl_gen,
statement_var,
statement_reassign,
statement_crash,
Expand Down
60 changes: 59 additions & 1 deletion src/canonicalize/NodeStore.zig
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ pub const MODULEENV_DIAGNOSTIC_NODE_COUNT = 59;
/// Count of the expression nodes in the ModuleEnv
pub const MODULEENV_EXPR_NODE_COUNT = 36;
/// Count of the statement nodes in the ModuleEnv
pub const MODULEENV_STATEMENT_NODE_COUNT = 15;
pub const MODULEENV_STATEMENT_NODE_COUNT = 16;
/// Count of the type annotation nodes in the ModuleEnv
pub const MODULEENV_TYPE_ANNO_NODE_COUNT = 12;
/// Count of the pattern nodes in the ModuleEnv
Expand Down Expand Up @@ -240,6 +240,22 @@ pub fn getStatement(store: *const NodeStore, statement: CIR.Statement.Idx) CIR.S
},
} };
},
.statement_decl_gen => {
return CIR.Statement{ .s_decl_gen = .{
.pattern = @enumFromInt(node.data_1),
.expr = @enumFromInt(node.data_2),
.anno = blk: {
const extra_start = node.data_3;
const extra_data = store.extra_data.items.items[extra_start..];
const has_anno = extra_data[0] != 0;
if (has_anno) {
break :blk @as(CIR.Annotation.Idx, @enumFromInt(extra_data[1]));
} else {
break :blk null;
}
},
} };
},
.statement_var => return CIR.Statement{ .s_var = .{
.pattern_idx = @enumFromInt(node.data_1),
.expr = @enumFromInt(node.data_2),
Expand Down Expand Up @@ -751,6 +767,34 @@ pub fn replaceExprWithNum(store: *NodeStore, expr_idx: CIR.Expr.Idx, value: CIR.
});
}

/// Replaces an existing expression with an e_zero_argument_tag expression in-place.
/// This is used for constant folding tag unions (like Bool) during compile-time evaluation.
/// Note: This modifies only the CIR node and should only be called after type-checking
/// is complete. Type information is stored separately and remains unchanged.
pub fn replaceExprWithZeroArgumentTag(
store: *NodeStore,
expr_idx: CIR.Expr.Idx,
closure_name: Ident.Idx,
variant_var: types.Var,
ext_var: types.Var,
name: Ident.Idx,
) !void {
const node_idx: Node.Idx = @enumFromInt(@intFromEnum(expr_idx));

const extra_data_start = store.extra_data.len();
_ = try store.extra_data.append(store.gpa, @bitCast(closure_name));
_ = try store.extra_data.append(store.gpa, @intFromEnum(variant_var));
_ = try store.extra_data.append(store.gpa, @intFromEnum(ext_var));
_ = try store.extra_data.append(store.gpa, @bitCast(name));

store.nodes.set(node_idx, .{
.tag = .expr_zero_argument_tag,
.data_1 = @intCast(extra_data_start),
.data_2 = 0,
.data_3 = 0,
});
}

/// Get the more-specific expr index. Used to make error messages nicer.
///
/// For example, if the provided expr is a `block`, then this will return the
Expand Down Expand Up @@ -1268,6 +1312,20 @@ fn makeStatementNode(store: *NodeStore, statement: CIR.Statement) Allocator.Erro
node.data_2 = @intFromEnum(s.expr);
node.data_3 = extra_data_start;
},
.s_decl_gen => |s| {
const extra_data_start: u32 = @intCast(store.extra_data.len());
if (s.anno) |anno| {
_ = try store.extra_data.append(store.gpa, @intFromBool(true));
_ = try store.extra_data.append(store.gpa, @intFromEnum(anno));
} else {
_ = try store.extra_data.append(store.gpa, @intFromBool(false));
}

node.tag = .statement_decl_gen;
node.data_1 = @intFromEnum(s.pattern);
node.data_2 = @intFromEnum(s.expr);
node.data_3 = extra_data_start;
},
.s_var => |s| {
node.tag = .statement_var;
node.data_1 = @intFromEnum(s.pattern_idx);
Expand Down
Loading
Loading