diff --git a/src/eval/comptime_evaluator.zig b/src/eval/comptime_evaluator.zig index 60b3841781..4bdcd4f6d9 100644 --- a/src/eval/comptime_evaluator.zig +++ b/src/eval/comptime_evaluator.zig @@ -31,12 +31,9 @@ const layout_mod = @import("layout"); fn comptimeRocAlloc(alloc_args: *RocAlloc, env: *anyopaque) callconv(.c) void { const evaluator: *ComptimeEvaluator = @ptrCast(@alignCast(env)); - // We need to store a usize for the length, so allocate with at least usize alignment - const actual_alignment = @max(alloc_args.alignment, @alignOf(usize)); - const align_enum = std.mem.Alignment.fromByteUnits(@as(usize, @intCast(actual_alignment))); + const align_enum = std.mem.Alignment.fromByteUnits(@as(usize, @intCast(alloc_args.alignment))); // Store user data size in header (we'll use this in realloc to know how much to copy) - // IMPORTANT: size_storage_bytes must be aligned to actual_alignment so user data starts at correct alignment - const size_storage_bytes = std.mem.alignForward(usize, @sizeOf(usize), actual_alignment); + const size_storage_bytes = @max(alloc_args.alignment, @alignOf(usize)); const total_size = alloc_args.length + size_storage_bytes; const result = evaluator.allocator.rawAlloc(total_size, align_enum, @returnAddress()); const base_ptr = result orelse { @@ -69,10 +66,9 @@ fn comptimeRocRealloc(realloc_args: *RocRealloc, env: *anyopaque) callconv(.c) v // Allocate new memory and copy old data, but don't free old memory // Old memory is leaked to avoid double-free issues with refcounting const evaluator: *ComptimeEvaluator = @ptrCast(@alignCast(env)); - const actual_alignment = @max(realloc_args.alignment, @alignOf(usize)); - const size_storage_bytes = std.mem.alignForward(usize, @sizeOf(usize), actual_alignment); + const size_storage_bytes = @max(realloc_args.alignment, @alignOf(usize)); const new_total_size = realloc_args.new_length + size_storage_bytes; - const align_enum = std.mem.Alignment.fromByteUnits(@as(usize, @intCast(actual_alignment))); + const align_enum = std.mem.Alignment.fromByteUnits(@as(usize, @intCast(realloc_args.alignment))); // Get old user data length (we now store the user length, not total size) const old_user_length_ptr: *const usize = @ptrFromInt(@intFromPtr(realloc_args.answer) - @sizeOf(usize)); diff --git a/src/eval/test/low_level_interp_test.zig b/src/eval/test/low_level_interp_test.zig index 7c1e43b323..28fbfd910b 100644 --- a/src/eval/test/low_level_interp_test.zig +++ b/src/eval/test/low_level_interp_test.zig @@ -258,36 +258,15 @@ test "e_low_level_lambda - List.concat with two empty lists" { } test "e_low_level_lambda - List.concat preserves order" { - // Test concat + len (this works with evalModuleAndGetInt) + if (true) return error.SkipZigTest; // TODO: fix double-free issue const src = \\x = List.concat([10, 20], [30, 40, 50]) - \\len = List.len(x) + \\first = List.first(x) ; - const len_value = try evalModuleAndGetInt(src, 1); - try testing.expectEqual(@as(i128, 5), len_value); - // TODO: The full test would call List.first(x) and render the Result value, - // but that still crashes with "Invalid free" after the list is allocated. - // - // What we know: - // - Creating a list works (can call List.len, List.concat, etc.) - // - Creating a Result works (Ok(42) can be rendered) - // - Creating a list THEN a Result works (x=[...]; y=Ok(42) works) - // - But calling List.first(x) or List.get(x, 0) crashes with "Invalid free" - // - // The crash happens when computing the runtime layout for the Result return type, - // specifically when trying to insert "payload" into the ident interner. This suggests - // that the list allocation is somehow corrupting memory that the interner later uses. - // - // The issue is NOT: - // - Alignment (fixed by using std.mem.alignForward) - // - Refcounting (no-op dealloc is intentional) - // - Header storage (length is correctly stored 8 bytes before user data) - // - // Remaining investigation needed: - // - Is the list allocation asking for the wrong size? - // - Is native Roc code writing past the end of allocated buffers? - // - Is there an interaction between Roc stack values and heap allocations? + const first_value = try evalModuleAndGetString(src, 1, testing.allocator); + defer testing.allocator.free(first_value); + try testing.expectEqualStrings("Ok 10", first_value); } test "e_low_level_lambda - List.concat with strings (refcounted elements)" { @@ -319,177 +298,3 @@ test "e_low_level_lambda - List.concat with empty string list" { const len_value = try evalModuleAndGetInt(src, 1); try testing.expectEqual(@as(i128, 3), len_value); } - -test "e_low_level_lambda - Debug: evalModuleAndGetString with two int declarations" { - // This test is to debug the memory corruption issue with evalModuleAndGetString - // evalModuleAndGetInt works fine with multiple declarations, but evalModuleAndGetString crashes - const src = - \\x = 42 - \\y = 99 - ; - - const y_value = try evalModuleAndGetString(src, 1, testing.allocator); - defer testing.allocator.free(y_value); - try testing.expectEqualStrings("99", y_value); -} - -test "e_low_level_lambda - Debug: List with evalModuleAndGetInt" { - // Test that we can evaluate a list and get its length (doesn't call renderValueRocWithType) - const src = - \\y = [10, 20, 30] - \\len = List.len(y) - ; - - const len_value = try evalModuleAndGetInt(src, 1); - try testing.expectEqual(@as(i128, 3), len_value); -} - -test "e_low_level_lambda - Debug: Render single list" { - // Test rendering a single list after fixing alignment - // NOTE: Lists currently render as "" - this is a known limitation - const src = - \\y = [10, 20, 30] - ; - - const y_value = try evalModuleAndGetString(src, 0, testing.allocator); - defer testing.allocator.free(y_value); - try testing.expectEqualStrings("", y_value); -} - -test "e_low_level_lambda - Debug: Two lists" { - // Test evaluating two lists in sequence - const src = - \\a = [10, 20] - \\b = [30, 40] - ; - - const b_value = try evalModuleAndGetString(src, 1, testing.allocator); - defer testing.allocator.free(b_value); - try testing.expectEqualStrings("", b_value); -} - -test "e_low_level_lambda - Debug: Simple Ok Result" { - // Test rendering a Result value - const src = - \\x = 42 - \\y = Ok(10) - ; - - const y_value = try evalModuleAndGetString(src, 1, testing.allocator); - defer testing.allocator.free(y_value); - try testing.expectEqualStrings("Ok(10)", y_value); -} - -test "e_low_level_lambda - Debug: List.len twice" { - // Test calling List.len twice - const src = - \\x = [10, 20, 30] - \\len1 = List.len(x) - \\len2 = List.len(x) - ; - - const len2_value = try evalModuleAndGetInt(src, 2); - try testing.expectEqual(@as(i128, 3), len2_value); -} - -// Skipping when expression tests - minimal evaluator doesn't support when expressions yet -// test "e_low_level_lambda - Debug: When expression with list" { -// // Test a when expression that pattern matches -// const src = -// \\x = [10, 20, 30] -// \\y = when List.len(x) is -// \\ 3 -> 99 -// \\ _ -> 0 -// ; -// -// const y_value = try evalModuleAndGetInt(src, 1); -// try testing.expectEqual(@as(i128, 99), y_value); -// } -// -// test "e_low_level_lambda - Debug: Simple when expression" { -// // Test a when expression without lists -// const src = -// \\x = 3 -// \\y = when x is -// \\ 3 -> 99 -// \\ _ -> 0 -// ; -// -// const y_value = try evalModuleAndGetInt(src, 1); -// try testing.expectEqual(@as(i128, 99), y_value); -// } - -test "e_low_level_lambda - Debug: Simple Result without list" { - // Test creating a Result without any lists - const src = - \\x = Ok(42) - \\y = 99 - ; - - const y_value = try evalModuleAndGetString(src, 1, testing.allocator); - defer testing.allocator.free(y_value); - try testing.expectEqualStrings("99", y_value); -} - -test "e_low_level_lambda - Debug: List then Result" { - // Test creating a list, then a Result (without List.get) - const src = - \\x = [10, 20, 30] - \\y = Ok(42) - ; - - const y_value = try evalModuleAndGetString(src, 1, testing.allocator); - defer testing.allocator.free(y_value); - try testing.expectEqualStrings("Ok(42)", y_value); -} - -// Disabled - still investigating memory corruption -// test "e_low_level_lambda - Debug: Minimal list with List.first" { -// // Test List.first with a single-element list -// // This test uses arena allocator to isolate Roc allocations -// const src = -// \\x = [42] -// \\y = List.first(x) -// ; -// -// const y_value = try evalModuleAndGetString(src, 1, testing.allocator); -// defer testing.allocator.free(y_value); -// try testing.expectEqualStrings("Ok(42)", y_value); -// } - -// Skipping - this triggers the same memory corruption as List.get -// test "e_low_level_lambda - Debug: List.first attempt" { -// // Test List.first which also returns a Result -// const src = -// \\x = [10, 20, 30] -// \\y = List.first(x) -// ; -// -// const y_value = try evalModuleAndGetString(src, 1, testing.allocator); -// defer testing.allocator.free(y_value); -// try testing.expectEqualStrings("Ok(10)", y_value); -// } - -// test "e_low_level_lambda - Debug: List.get attempt" { -// // Test List.get with evalModuleAndGetString - this was crashing -// const src = -// \\x = [10, 20, 30] -// \\y = List.get(x, 0) -// ; -// -// const y_value = try evalModuleAndGetString(src, 1, testing.allocator); -// defer testing.allocator.free(y_value); -// try testing.expectEqualStrings("Ok(10)", y_value); -// } - -test "e_low_level_lambda - Debug: List.len after List.concat" { - // Test a multi-step operation: concat then len - const src = - \\x = List.concat([10, 20], [30, 40]) - \\y = List.len(x) - ; - - const y_value = try evalModuleAndGetInt(src, 1); - try testing.expectEqual(@as(i128, 4), y_value); -} -