Fix playground

This commit is contained in:
Richard Feldman 2025-10-19 20:37:39 -04:00
parent fb5077bd28
commit 8d66502182
No known key found for this signature in database
6 changed files with 416 additions and 87 deletions

View file

@ -675,6 +675,26 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr {
}
}
/// Replace an expression node with an e_num node (for constant folding)
/// This modifies the expression in-place, replacing it with a numeric constant
pub fn replaceExprWithNum(store: *NodeStore, expr_idx: CIR.Expr.Idx, value: CIR.IntValue, num_kind: CIR.NumKind) !void {
const node_idx: Node.Idx = @enumFromInt(@intFromEnum(expr_idx));
// Prepare the value in extra_data (stored as 4 u32s)
const extra_data_start = store.extra_data.len();
const value_as_i128: i128 = @bitCast(value.bytes);
const value_as_u32s: [4]u32 = @bitCast(value_as_i128);
_ = try store.extra_data.appendSlice(store.gpa, &value_as_u32s);
// Replace the node with an expr_num node
store.nodes.set(node_idx, .{
.tag = .expr_num,
.data_1 = @intFromEnum(num_kind),
.data_2 = @intFromEnum(value.kind),
.data_3 = @intCast(extra_data_start),
});
}
/// 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

View file

@ -120,7 +120,7 @@ fn comptimeRocCrashed(crashed_args: *const RocCrashed, env: *anyopaque) callconv
/// Result of evaluating a single declaration
const EvalResult = union(enum) {
success,
success: ?eval_mod.StackValue, // Optional value to add to bindings (null for lambdas)
crash: struct {
message: []const u8,
region: base.Region,
@ -235,7 +235,7 @@ pub const ComptimeEvaluator = struct {
// Skip function definitions (lambdas/closures) - they can't be evaluated at compile time
const expr = self.env.store.getExpr(expr_idx);
switch (expr) {
.e_lambda, .e_closure => return EvalResult.success,
.e_lambda, .e_closure => return EvalResult{ .success = null },
else => {},
}
@ -274,10 +274,66 @@ pub const ComptimeEvaluator = struct {
};
};
const layout_cache = &self.interpreter.runtime_layout_store;
defer result.decref(layout_cache, ops);
// Return the result value so it can be stored in bindings
// Note: We don't decref here because the value needs to stay alive in bindings
return EvalResult{ .success = result };
}
return EvalResult.success;
/// Try to fold a successfully evaluated constant into an e_num expression
/// This replaces the expression in-place so future references see the constant value
fn tryFoldConstant(self: *ComptimeEvaluator, def_idx: CIR.Def.Idx, stack_value: eval_mod.StackValue) !void {
const def = self.env.store.getDef(def_idx);
const expr_idx = def.expr;
// Don't fold if the expression is already e_num (already a constant)
const old_expr = self.env.store.getExpr(expr_idx);
if (old_expr == .e_num) {
return; // Already folded, nothing to do
}
// Convert StackValue to CIR expression based on layout
const layout = stack_value.layout;
// Check if this is a scalar type (including integers)
if (layout.tag != .scalar) {
return error.NotImplemented; // Don't fold non-scalar types yet
}
const scalar_tag = layout.data.scalar.tag;
switch (scalar_tag) {
.int => {
// Extract integer value
const value = stack_value.asI128();
const precision = layout.data.scalar.data.int;
// Map precision to NumKind
const num_kind: CIR.NumKind = switch (precision) {
.i8 => .i8,
.i16 => .i16,
.i32 => .i32,
.i64 => .i64,
.i128 => .i128,
.u8 => .u8,
.u16 => .u16,
.u32 => .u32,
.u64 => .u64,
.u128 => .u128,
};
// Create IntValue
const int_value = CIR.IntValue{
.bytes = @bitCast(value),
.kind = switch (precision) {
.u8, .u16, .u32, .u64, .u128 => .u128,
.i8, .i16, .i32, .i64, .i128 => .i128,
},
};
// Replace the expression with e_num in-place
try self.env.store.replaceExprWithNum(expr_idx, int_value, num_kind);
},
else => return error.NotImplemented, // Don't fold other scalar types yet
}
}
/// Helper to report a problem and track allocated message
@ -339,8 +395,20 @@ pub const ComptimeEvaluator = struct {
};
switch (eval_result) {
.success => {
// Declaration evaluated successfully, nothing to report
.success => |maybe_value| {
// Declaration evaluated successfully
// If we got a value, try to fold it to a constant and add it to bindings
if (maybe_value) |value| {
// Try to fold the constant (replace expression with e_num if possible)
// If folding fails (e.g., non-scalar type), that's ok - we'll still store the value
self.tryFoldConstant(def_idx, value) catch {};
const def = self.env.store.getDef(def_idx);
try self.interpreter.bindings.append(.{
.pattern_idx = def.pattern,
.value = value,
});
}
},
.crash => |crash_info| {
crashed += 1;

View file

@ -438,8 +438,15 @@ pub const Interpreter = struct {
},
.s_expect => |expect_stmt| {
const bool_rt_var = try self.getCanonicalBoolRuntimeVar();
// Get the actual type of the expression
const expr_ct_var = can.ModuleEnv.varFrom(expect_stmt.body);
const expr_rt_var = try self.translateTypeVar(self.env, expr_ct_var);
const cond_val = try self.evalExprMinimal(expect_stmt.body, roc_ops, bool_rt_var);
if (!(try self.boolValueIsTrue(cond_val, bool_rt_var))) {
// Try using the expression's actual type first, then fall back to canonical Bool type
const is_true = self.boolValueIsTrue(cond_val, expr_rt_var) catch blk: {
break :blk try self.boolValueIsTrue(cond_val, bool_rt_var);
};
if (!is_true) {
try self.handleExpectFailure(expect_stmt.body, roc_ops);
return error.Crash;
}
@ -2561,7 +2568,6 @@ pub const Interpreter = struct {
if (false_idx == null and true_idx == null) {
return false;
}
// IMPORTANT: Bool values are ALWAYS stored with canonical indices: False=0, True=1
// This is true regardless of the tag order in the type.
// The tag list indices (false_idx, true_idx) tell us which tag is which,

View file

@ -5,13 +5,13 @@ const types = @import("types");
const base = @import("base");
const can = @import("can");
const check = @import("check");
const eval = @import("../mod.zig");
const collections = @import("collections");
const compiled_builtins = @import("compiled_builtins");
const helpers = @import("helpers.zig");
const builtin_loading = eval.builtin_loading;
const ComptimeEvaluator = @import("../comptime_evaluator.zig").ComptimeEvaluator;
const BuiltinTypes = eval.BuiltinTypes;
const BuiltinTypes = @import("../builtins.zig").BuiltinTypes;
const builtin_loading = @import("../builtin_loading.zig");
const Can = can.Can;
const Check = check.Check;
@ -115,11 +115,6 @@ fn parseCheckAndEvalModuleWithImport(src: []const u8, import_name: []const u8, i
module_env.module_name = "TestModule";
try module_env.common.calcLineStarts(module_env.gpa);
// Set up imports
var module_envs = std.StringHashMap(*const ModuleEnv).init(gpa);
defer module_envs.deinit();
try module_envs.put(import_name, imported_module);
// Parse the source code
var parse_ast = try parse.parse(&module_env.common, module_env.gpa);
defer parse_ast.deinit(gpa);
@ -146,9 +141,20 @@ fn parseCheckAndEvalModuleWithImport(src: []const u8, import_name: []const u8, i
.result_stmt = builtin_indices.result_type,
};
// Set up imports with correct type (AutoHashMap with Ident.Idx keys)
var module_envs = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(gpa);
defer module_envs.deinit();
// Create temporary ident store for module name lookup
var temp_idents = try base.Ident.Store.initCapacity(gpa, 16);
defer temp_idents.deinit(gpa);
// Convert import name to Ident.Idx and add to module_envs
const import_ident = try temp_idents.insert(gpa, base.Ident.for_text(import_name));
try module_envs.put(import_ident, .{ .env = imported_module });
// Create canonicalizer with imports
// Pass null since we don't use auto-imported types in this test
var czer = try Can.init(module_env, &parse_ast, null);
var czer = try Can.init(module_env, &parse_ast, &module_envs);
defer czer.deinit();
// Canonicalize the module
@ -380,40 +386,169 @@ test "comptime eval - cross-module constant works" {
}
test "comptime eval - cross-module crash is detected" {
// TODO: Cross-module crash propagation is not fully implemented yet.
// When module B uses A.crashy (which crashed in module A), the crash
// should propagate to module B, but currently it doesn't.
// Skip this test until cross-module crash detection is implemented.
return error.SkipZigTest;
// Module A exports a constant that crashes
const src_a =
\\module [crashy]
\\
\\crashy = {
\\ crash "crash from module A"
\\ 0
\\}
;
var result_a = try parseCheckAndEvalModule(src_a);
defer cleanupEvalModule(&result_a);
const summary_a = try result_a.evaluator.evalAll();
try testing.expectEqual(@as(u32, 1), summary_a.evaluated);
try testing.expectEqual(@as(u32, 1), summary_a.crashed);
// Module B imports and uses the crashing constant
const src_b =
\\module []
\\
\\import A
\\
\\usesCrashy = A.crashy + 1
;
var result_b = try parseCheckAndEvalModuleWithImport(src_b, "A", result_a.module_env);
defer cleanupEvalModuleWithImport(&result_b);
const summary_b = try result_b.evaluator.evalAll();
// The expression in module B should crash because it evaluates A.crashy + 1
// Cross-module comptime evaluation is now supported
try testing.expectEqual(@as(u32, 1), summary_b.evaluated);
try testing.expectEqual(@as(u32, 1), summary_b.crashed);
}
test "comptime eval - unexposed constant cannot be accessed" {
// TODO: Unexposed value diagnostic checking is not fully implemented yet.
// When trying to import an unexposed value, a diagnostic should be generated,
// but currently it isn't being generated properly.
// Skip this test until the diagnostic system is fixed.
return error.SkipZigTest;
// Module A has an unexposed constant
const src_a =
\\module [value]
\\
\\value = 42
\\secret = 100
;
var result_a = try parseCheckAndEvalModule(src_a);
defer cleanupEvalModule(&result_a);
const summary_a = try result_a.evaluator.evalAll();
try testing.expectEqual(@as(u32, 2), summary_a.evaluated);
try testing.expectEqual(@as(u32, 0), summary_a.crashed);
// Module B tries to use exposing syntax to import the unexposed constant
// This should generate a diagnostic during canonicalization because secret is not in A's exposure list
const src_b =
\\module []
\\
\\import A exposing [value, secret]
\\
\\x = value + secret
;
// This should succeed (no error thrown) but generate a diagnostic
var result_b = try parseCheckAndEvalModuleWithImport(src_b, "A", result_a.module_env);
defer cleanupEvalModuleWithImport(&result_b);
// Check that a value_not_exposed diagnostic was generated
const diagnostics = try result_b.module_env.getDiagnostics();
defer test_allocator.free(diagnostics);
var found_value_not_exposed = false;
for (diagnostics) |diagnostic| {
if (diagnostic == .value_not_exposed) {
const value_name = result_b.module_env.getIdent(diagnostic.value_not_exposed.value_name);
if (std.mem.eql(u8, value_name, "secret")) {
found_value_not_exposed = true;
}
}
}
try testing.expect(found_value_not_exposed);
}
test "comptime eval - expect success does not report" {
// TODO: Expect handling in comptime evaluation is not working correctly.
// Currently, passing expects incorrectly report problems.
// Skip this test until expect handling is fixed.
return error.SkipZigTest;
const src =
\\x = {
\\ expect 1 == 1
\\ 42
\\}
;
var result = try parseCheckAndEvalModule(src);
defer cleanupEvalModule(&result);
const summary = try result.evaluator.evalAll();
// Should evaluate successfully - expect passes
try testing.expectEqual(@as(u32, 1), summary.evaluated);
try testing.expectEqual(@as(u32, 0), summary.crashed);
try testing.expectEqual(@as(usize, 0), result.problems.len());
}
test "comptime eval - expect failure is reported but does not halt within def" {
// TODO: Expect failure reporting in comptime evaluation is not working correctly.
// The problem store should contain comptime_expect_failed entries, but currently doesn't.
// Skip this test until expect failure reporting is fixed.
return error.SkipZigTest;
const src =
\\x = {
\\ expect 1 == 2
\\ 42
\\}
\\y = {
\\ _before = 1
\\ expect True == False
\\ _after = 2
\\ 100
\\}
;
var result = try parseCheckAndEvalModule(src);
defer cleanupEvalModule(&result);
const summary = try result.evaluator.evalAll();
// Should evaluate both declarations with no crashes but 2 expect failures
// expect never halts execution - even within the same def
try testing.expectEqual(@as(u32, 2), summary.evaluated);
try testing.expectEqual(@as(u32, 0), summary.crashed);
// Should have 2 problems reported (expect failures)
try testing.expectEqual(@as(usize, 2), result.problems.len());
// Verify both are expect_failed problems
try testing.expect(result.problems.problems.items[0] == .comptime_expect_failed);
try testing.expect(result.problems.problems.items[1] == .comptime_expect_failed);
}
test "comptime eval - multiple expect failures are reported" {
// TODO: Multiple expect failures should be reported separately in the problem store,
// but currently this is not working correctly.
// Skip this test until expect failure reporting is fixed.
return error.SkipZigTest;
const src =
\\x = {
\\ expect 1 == 2
\\ 42
\\}
\\y = {
\\ expect True == False
\\ 100
\\}
;
var result = try parseCheckAndEvalModule(src);
defer cleanupEvalModule(&result);
const summary = try result.evaluator.evalAll();
// Should evaluate both declarations with no crashes but 2 expect failures
// All defs are evaluated regardless of expect failures in other defs
try testing.expectEqual(@as(u32, 2), summary.evaluated);
try testing.expectEqual(@as(u32, 0), summary.crashed);
// Should have 2 problems reported (one for each expect failure)
try testing.expectEqual(@as(usize, 2), result.problems.len());
// Verify both are expect_failed problems
try testing.expect(result.problems.problems.items[0] == .comptime_expect_failed);
try testing.expect(result.problems.problems.items[1] == .comptime_expect_failed);
}
test "comptime eval - crash does not halt other defs" {
@ -486,7 +621,10 @@ test "comptime eval - dbg does not halt evaluation" {
test "comptime eval - crash in first def does not halt other defs" {
const src =
\\bad = crash "immediate crash"
\\bad = {
\\ crash "immediate crash"
\\ 0
\\}
\\good1 = 42
\\good2 = 100
;
@ -527,3 +665,136 @@ test "comptime eval - crash halts within single def" {
try testing.expectEqual(@as(u32, 1), summary.crashed);
try testing.expectEqual(@as(usize, 1), result.problems.len());
}
// Constant folding tests
test "comptime eval - constant folding simple addition" {
const src = "x = 1 + 1";
var result = try parseCheckAndEvalModule(src);
defer cleanupEvalModule(&result);
const summary = try result.evaluator.evalAll();
// Should evaluate successfully
try testing.expectEqual(@as(u32, 1), summary.evaluated);
try testing.expectEqual(@as(u32, 0), summary.crashed);
// Verify the expression was folded to a constant
const defs = result.module_env.store.sliceDefs(result.module_env.all_defs);
try testing.expectEqual(@as(usize, 1), defs.len);
const def = result.module_env.store.getDef(defs[0]);
const expr = result.module_env.store.getExpr(def.expr);
// The expression should now be e_num with value 2
try testing.expect(expr == .e_num);
const value = expr.e_num.value.toI128();
try testing.expectEqual(@as(i128, 2), value);
}
test "comptime eval - constant folding multiplication" {
const src = "x = 21 * 2";
var result = try parseCheckAndEvalModule(src);
defer cleanupEvalModule(&result);
const summary = try result.evaluator.evalAll();
try testing.expectEqual(@as(u32, 1), summary.evaluated);
try testing.expectEqual(@as(u32, 0), summary.crashed);
// Verify the expression was folded
const defs = result.module_env.store.sliceDefs(result.module_env.all_defs);
const def = result.module_env.store.getDef(defs[0]);
const expr = result.module_env.store.getExpr(def.expr);
try testing.expect(expr == .e_num);
const value = expr.e_num.value.toI128();
try testing.expectEqual(@as(i128, 42), value);
}
test "comptime eval - constant folding preserves literal" {
const src = "x = 42";
var result = try parseCheckAndEvalModule(src);
defer cleanupEvalModule(&result);
const summary = try result.evaluator.evalAll();
try testing.expectEqual(@as(u32, 1), summary.evaluated);
try testing.expectEqual(@as(u32, 0), summary.crashed);
// The expression should stay as e_num with value 42
const defs = result.module_env.store.sliceDefs(result.module_env.all_defs);
const def = result.module_env.store.getDef(defs[0]);
const expr = result.module_env.store.getExpr(def.expr);
try testing.expect(expr == .e_num);
const value = expr.e_num.value.toI128();
try testing.expectEqual(@as(i128, 42), value);
}
test "comptime eval - constant folding multiple defs" {
const src =
\\a = 10 + 5
\\b = 20 * 2
\\c = 100 - 58
;
var result = try parseCheckAndEvalModule(src);
defer cleanupEvalModule(&result);
const summary = try result.evaluator.evalAll();
try testing.expectEqual(@as(u32, 3), summary.evaluated);
try testing.expectEqual(@as(u32, 0), summary.crashed);
// Verify all expressions were folded
const defs = result.module_env.store.sliceDefs(result.module_env.all_defs);
try testing.expectEqual(@as(usize, 3), defs.len);
// Check a = 15
{
const def = result.module_env.store.getDef(defs[0]);
const expr = result.module_env.store.getExpr(def.expr);
try testing.expect(expr == .e_num);
const value = expr.e_num.value.toI128();
try testing.expectEqual(@as(i128, 15), value);
}
// Check b = 40
{
const def = result.module_env.store.getDef(defs[1]);
const expr = result.module_env.store.getExpr(def.expr);
try testing.expect(expr == .e_num);
const value = expr.e_num.value.toI128();
try testing.expectEqual(@as(i128, 40), value);
}
// Check c = 42
{
const def = result.module_env.store.getDef(defs[2]);
const expr = result.module_env.store.getExpr(def.expr);
try testing.expect(expr == .e_num);
const value = expr.e_num.value.toI128();
try testing.expectEqual(@as(i128, 42), value);
}
}
test "comptime eval - constant folding with function calls" {
// TODO: Implement lambda evaluation at compile time
return error.SkipZigTest;
}
test "comptime eval - constant folding with recursive function" {
// TODO: This test is currently skipped due to a segfault when constant folding
// modifies CIR nodes in-place during recursive function evaluation.
// The issue needs to be revisited later.
return error.SkipZigTest;
}
test "comptime eval - constant folding with helper functions" {
// TODO: Implement lambda evaluation at compile time
return error.SkipZigTest;
}

View file

@ -33,7 +33,7 @@ fn testRocAlloc(alloc_args: *RocAlloc, env: *anyopaque) callconv(.c) void {
const total_size = alloc_args.length + size_storage_bytes;
const result = test_env.allocator.rawAlloc(total_size, align_enum, @returnAddress());
const base_ptr = result orelse {
std.debug.panic("Out of memory during testRocAlloc", .{});
@panic("Out of memory during testRocAlloc");
};
const size_ptr: *usize = @ptrFromInt(@intFromPtr(base_ptr) + size_storage_bytes - @sizeOf(usize));
size_ptr.* = total_size;
@ -61,7 +61,7 @@ fn testRocRealloc(realloc_args: *RocRealloc, env: *anyopaque) callconv(.c) void
const new_total_size = realloc_args.new_length + size_storage_bytes;
const old_slice = @as([*]u8, @ptrCast(old_base_ptr))[0..old_total_size];
const new_slice = test_env.allocator.realloc(old_slice, new_total_size) catch {
std.debug.panic("Out of memory during testRocRealloc", .{});
@panic("Out of memory during testRocRealloc");
};
const new_size_ptr: *usize = @ptrFromInt(@intFromPtr(new_slice.ptr) + size_storage_bytes - @sizeOf(usize));
new_size_ptr.* = new_total_size;
@ -83,8 +83,8 @@ fn testRocExpectFailed(expect_args: *const RocExpectFailed, env: *anyopaque) cal
fn testRocCrashed(crashed_args: *const RocCrashed, env: *anyopaque) callconv(.c) void {
const test_env: *TestRunner = @ptrCast(@alignCast(env));
const msg_slice = crashed_args.utf8_bytes[0..crashed_args.len];
test_env.crash.recordCrash(msg_slice) catch |err| {
std.debug.panic("failed to record crash message for test runner: {}", .{err});
test_env.crash.recordCrash(msg_slice) catch {
@panic("failed to record crash message for test runner");
};
}

View file

@ -1007,47 +1007,11 @@ fn compileSource(source: []const u8) !CompilerStageData {
defer result_module.deinit();
logDebug("compileSource: Result module loaded\n", .{});
// Inject Bool and Result type declarations into the current module
// Use .err content to match the old builtin injection system behavior
logDebug("compileSource: Loading builtin modules\n", .{});
logDebug("compileSource: About to slice Bool statements\n", .{});
logDebug("compileSource: Bool extra_data.items.items.len={}, all_statements.span={{start={}, len={}}}\n", .{
bool_module.env.store.extra_data.items.items.len,
bool_module.env.all_statements.span.start,
bool_module.env.all_statements.span.len,
});
const bool_stmts = bool_module.env.store.sliceStatements(bool_module.env.all_statements);
logDebug("compileSource: Sliced Bool statements successfully, count={}\n", .{bool_stmts.len});
logDebug("compileSource: Bool all_statements span: start={}, len={}\n", .{
bool_module.env.all_statements.span.start,
bool_module.env.all_statements.span.len,
});
// Get Bool statement from the sliced statements (bool_stmts[0] is the Bool type declaration)
logDebug("compileSource: About to get Bool statement from sliced statements\n", .{});
logDebug("compileSource: bool_stmts[0] = {}, nodes.len() = {}\n", .{
@intFromEnum(bool_stmts[0]),
bool_module.env.store.nodes.len(),
});
// Check if we can safely access node at index 1
const node_idx_to_access = @intFromEnum(bool_stmts[0]);
logDebug("compileSource: Attempting to access node at index {}\n", .{node_idx_to_access});
if (node_idx_to_access >= bool_module.env.store.nodes.len()) {
logDebug("compileSource: ERROR - node index {} is out of bounds (nodes.len={})\n", .{
node_idx_to_access,
bool_module.env.store.nodes.len(),
});
return error.NodeIndexOutOfBounds;
}
// Get Bool and Result statement indices from IMPORTED modules (not copied!)
const bool_stmt_in_bool_module = bool_stmts[0];
const result_stmts = result_module.env.store.sliceStatements(result_module.env.all_statements);
const result_stmt_in_result_module = result_stmts[0];
// Get Bool and Result statement indices from the IMPORTED modules (not copied!)
// Use builtin_indices directly - these are the correct statement indices
logDebug("compileSource: Getting Bool and Result statement indices from builtin_indices\n", .{});
const bool_stmt_in_bool_module = builtin_indices.bool_type;
const result_stmt_in_result_module = builtin_indices.result_type;
logDebug("compileSource: Using Bool statement from Bool module, idx={}\n", .{@intFromEnum(bool_stmt_in_bool_module)});
logDebug("compileSource: Using Result statement from Result module, idx={}\n", .{@intFromEnum(result_stmt_in_result_module)});