From e0f3a4706b64281afbed93daf5eef9f91c8f8b8a Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 24 Nov 2025 20:20:58 -0500 Subject: [PATCH 1/8] Fix numeral bug --- src/cli/main.zig | 55 +++++++---- src/cli/test/fx_platform_test.zig | 80 ++++++++++++++++ src/eval/interpreter.zig | 154 +++++++++++++++++++++++++++++- src/eval/test_runner.zig | 4 +- src/playground_wasm/main.zig | 4 +- test/fx/expect_with_literal.roc | 14 +++ test/fx/expect_with_main.roc | 16 ++++ 7 files changed, 302 insertions(+), 25 deletions(-) create mode 100644 test/fx/expect_with_literal.roc create mode 100644 test/fx/expect_with_main.roc diff --git a/src/cli/main.zig b/src/cli/main.zig index 65bac5487a..3e249876c5 100644 --- a/src/cli/main.zig +++ b/src/cli/main.zig @@ -2744,15 +2744,31 @@ fn rocTest(allocs: *Allocators, args: cli_args.TestArgs) !void { env.module_name = module_name; try env.common.calcLineStarts(allocs.gpa); + // Load builtin modules required by the type checker and interpreter + const builtin_indices = builtin_loading.deserializeBuiltinIndices(allocs.gpa, compiled_builtins.builtin_indices_bin) catch |err| { + try stderr.print("Failed to deserialize builtin indices: {}\n", .{err}); + return err; + }; + const builtin_source = compiled_builtins.builtin_source; + var builtin_module = builtin_loading.loadCompiledModule(allocs.gpa, compiled_builtins.builtin_bin, "Builtin", builtin_source) catch |err| { + try stderr.print("Failed to load Builtin module: {}\n", .{err}); + return err; + }; + defer builtin_module.deinit(); + + // Populate module_envs with Bool, Try, Dict, Set from builtin module + var module_envs = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(allocs.gpa); + defer module_envs.deinit(); + const module_common_idents: Check.CommonIdents = .{ .module_name = try env.insertIdent(base.Ident.for_text(module_name)), .list = try env.insertIdent(base.Ident.for_text("List")), .box = try env.insertIdent(base.Ident.for_text("Box")), .@"try" = try env.insertIdent(base.Ident.for_text("Try")), - .bool_stmt = @enumFromInt(0), // TODO: load from builtin modules - .try_stmt = @enumFromInt(0), // TODO: load from builtin modules - .str_stmt = @enumFromInt(0), // TODO: load from builtin modules - .builtin_module = null, + .bool_stmt = builtin_indices.bool_type, + .try_stmt = builtin_indices.try_type, + .str_stmt = builtin_indices.str_type, + .builtin_module = builtin_module.env, }; // Parse the source code as a full module @@ -2768,8 +2784,16 @@ fn rocTest(allocs: *Allocators, args: cli_args.TestArgs) !void { // Initialize CIR fields in ModuleEnv try env.initCIRFields(allocs.gpa, module_name); + // Populate module_envs with Bool, Try, Dict, Set using shared function + try Can.populateModuleEnvs( + &module_envs, + &env, + builtin_module.env, + builtin_indices, + ); + // Create canonicalizer - var canonicalizer = Can.init(&env, &parse_ast, null) catch |err| { + var canonicalizer = Can.init(&env, &parse_ast, &module_envs) catch |err| { try stderr.print("Failed to initialize canonicalizer: {}\n", .{err}); return err; }; @@ -2787,8 +2811,11 @@ fn rocTest(allocs: *Allocators, args: cli_args.TestArgs) !void { return err; }; + // Build imported_envs array with builtin module + const imported_envs: []const *const ModuleEnv = &.{builtin_module.env}; + // Type check the module - var checker = Check.init(allocs.gpa, &env.types, &env, &.{}, null, &env.store.regions, module_common_idents) catch |err| { + var checker = Check.init(allocs.gpa, &env.types, &env, imported_envs, &module_envs, &env.store.regions, module_common_idents) catch |err| { try stderr.print("Failed to initialize type checker: {}\n", .{err}); return err; }; @@ -2800,20 +2827,8 @@ fn rocTest(allocs: *Allocators, args: cli_args.TestArgs) !void { }; // Evaluate all top-level declarations at compile time - // Load builtin modules required by the interpreter - const builtin_indices = builtin_loading.deserializeBuiltinIndices(allocs.gpa, compiled_builtins.builtin_indices_bin) catch |err| { - try stderr.print("Failed to deserialize builtin indices: {}\n", .{err}); - return err; - }; - const builtin_source = compiled_builtins.builtin_source; - var builtin_module = builtin_loading.loadCompiledModule(allocs.gpa, compiled_builtins.builtin_bin, "Builtin", builtin_source) catch |err| { - try stderr.print("Failed to load Builtin module: {}\n", .{err}); - return err; - }; - defer builtin_module.deinit(); - const builtin_types_for_eval = BuiltinTypes.init(builtin_indices, builtin_module.env, builtin_module.env, builtin_module.env); - var comptime_evaluator = eval.ComptimeEvaluator.init(allocs.gpa, &env, &.{}, &checker.problems, builtin_types_for_eval, builtin_module.env) catch |err| { + var comptime_evaluator = eval.ComptimeEvaluator.init(allocs.gpa, &env, imported_envs, &checker.problems, builtin_types_for_eval, builtin_module.env) catch |err| { try stderr.print("Failed to create compile-time evaluator: {}\n", .{err}); return err; }; @@ -2826,7 +2841,7 @@ fn rocTest(allocs: *Allocators, args: cli_args.TestArgs) !void { }; // Create test runner infrastructure for test evaluation (reuse builtin_types_for_eval from above) - var test_runner = TestRunner.init(allocs.gpa, &env, builtin_types_for_eval) catch |err| { + var test_runner = TestRunner.init(allocs.gpa, &env, builtin_types_for_eval, imported_envs, builtin_module.env) catch |err| { try stderr.print("Failed to create test runner: {}\n", .{err}); return err; }; diff --git a/src/cli/test/fx_platform_test.zig b/src/cli/test/fx_platform_test.zig index 2cb52c733a..c405a9456c 100644 --- a/src/cli/test/fx_platform_test.zig +++ b/src/cli/test/fx_platform_test.zig @@ -184,3 +184,83 @@ test "fx platform stdin simple" { // stdin_simple reads from stdin and prints to stderr try testing.expect(std.mem.indexOf(u8, result.stderr, "simple test") != null); } + +test "fx platform expect with main" { + const allocator = testing.allocator; + + try ensureRocBinary(allocator); + + // Run `roc test` on the app that has both main! and an expect + // Note: `roc test` only evaluates expect statements, it does not run main! + const run_result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + "./zig-out/bin/roc", + "test", + "test/fx/expect_with_main.roc", + }, + }); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + switch (run_result.term) { + .Exited => |code| { + if (code != 0) { + std.debug.print("Run failed with exit code {}\n", .{code}); + std.debug.print("STDOUT: {s}\n", .{run_result.stdout}); + std.debug.print("STDERR: {s}\n", .{run_result.stderr}); + return error.RunFailed; + } + }, + else => { + std.debug.print("Run terminated abnormally: {}\n", .{run_result.term}); + std.debug.print("STDOUT: {s}\n", .{run_result.stdout}); + std.debug.print("STDERR: {s}\n", .{run_result.stderr}); + return error.RunFailed; + }, + } + + // When all tests pass without --verbose, roc test produces no output + try testing.expectEqualStrings("", run_result.stdout); + try testing.expectEqualStrings("", run_result.stderr); +} + +test "fx platform expect with numeric literal" { + const allocator = testing.allocator; + + try ensureRocBinary(allocator); + + // Run `roc test` on an app that compares a typed variable with a numeric literal + // This tests that numeric literals in top-level expects are properly typed + const run_result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + "./zig-out/bin/roc", + "test", + "test/fx/expect_with_literal.roc", + }, + }); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + switch (run_result.term) { + .Exited => |code| { + if (code != 0) { + std.debug.print("Run failed with exit code {}\n", .{code}); + std.debug.print("STDOUT: {s}\n", .{run_result.stdout}); + std.debug.print("STDERR: {s}\n", .{run_result.stderr}); + return error.RunFailed; + } + }, + else => { + std.debug.print("Run terminated abnormally: {}\n", .{run_result.term}); + std.debug.print("STDOUT: {s}\n", .{run_result.stdout}); + std.debug.print("STDERR: {s}\n", .{run_result.stderr}); + return error.RunFailed; + }, + } + + // When all tests pass without --verbose, roc test produces no output + try testing.expectEqualStrings("", run_result.stdout); + try testing.expectEqualStrings("", run_result.stderr); +} diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index 8dd9b51298..a8a6037eef 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -5565,6 +5565,59 @@ pub const Interpreter = struct { return error.MethodNotFound; } + /// Evaluate a numeric literal expression with a specific layout. + /// This is used when we know the target type from context (e.g., comparing with a typed value). + fn evalNumLitWithLayout( + self: *Interpreter, + expr_idx: can.CIR.Expr.Idx, + target_layout: layout.Layout, + roc_ops: *RocOps, + ) Error!StackValue { + _ = roc_ops; + const expr = self.env.store.getExpr(expr_idx); + if (expr != .e_num) { + return error.TypeMismatch; + } + const num_lit = expr.e_num; + + var value = try self.pushRaw(target_layout, 0); + value.is_initialized = false; + + switch (target_layout.tag) { + .scalar => switch (target_layout.data.scalar.tag) { + .int => try value.setIntFromBytes(num_lit.value.bytes, num_lit.value.kind == .u128), + .frac => switch (target_layout.data.scalar.data.frac) { + .f32 => { + const ptr = @as(*f32, @ptrCast(@alignCast(value.ptr.?))); + if (num_lit.value.kind == .u128) { + const u128_val: u128 = @bitCast(num_lit.value.bytes); + ptr.* = @floatFromInt(u128_val); + } else { + ptr.* = @floatFromInt(num_lit.value.toI128()); + } + }, + .f64 => { + const ptr = @as(*f64, @ptrCast(@alignCast(value.ptr.?))); + if (num_lit.value.kind == .u128) { + const u128_val: u128 = @bitCast(num_lit.value.bytes); + ptr.* = @floatFromInt(u128_val); + } else { + ptr.* = @floatFromInt(num_lit.value.toI128()); + } + }, + .dec => { + const ptr = @as(*RocDec, @ptrCast(@alignCast(value.ptr.?))); + ptr.* = .{ .num = num_lit.value.toI128() * RocDec.one_point_zero_i128 }; + }, + }, + else => return error.TypeMismatch, + }, + else => return error.TypeMismatch, + } + value.is_initialized = true; + return value; + } + /// Dispatch a binary operator to its corresponding method. /// Handles the full method dispatch including: /// - Type resolution with Dec default for flex/rigid vars @@ -5595,10 +5648,29 @@ pub const Interpreter = struct { lhs_resolved = self.runtime_types.resolveVar(dec_var); } - // Evaluate both operands + // Evaluate LHS first var lhs = try self.evalExprMinimal(lhs_expr, roc_ops, lhs_rt_var); defer lhs.decref(&self.runtime_layout_store, roc_ops); - var rhs = try self.evalExprMinimal(rhs_expr, roc_ops, rhs_rt_var); + + // For numeric binary operations, if LHS has a scalar layout (numeric) and RHS is a flex/rigid var, + // we need to use the LHS's actual layout to determine RHS type + const rhs_resolved = self.runtime_types.resolveVar(rhs_rt_var); + const rhs_expr_data = self.env.store.getExpr(rhs_expr); + + // Check if RHS is a numeric literal that needs to be coerced to match LHS + const rhs_is_flex = rhs_resolved.desc.content == .flex or rhs_resolved.desc.content == .rigid; + const lhs_is_numeric = lhs.layout.tag == .scalar and + (lhs.layout.data.scalar.tag == .int or lhs.layout.data.scalar.tag == .frac); + const should_coerce_rhs = rhs_is_flex and lhs_is_numeric and rhs_expr_data == .e_num; + + // Evaluate RHS with LHS's layout if coercion is needed + var rhs: StackValue = undefined; + if (should_coerce_rhs) { + // Evaluate the numeric literal using the LHS's layout type + rhs = try self.evalNumLitWithLayout(rhs_expr, lhs.layout, roc_ops); + } else { + rhs = try self.evalExprMinimal(rhs_expr, roc_ops, rhs_rt_var); + } defer rhs.decref(&self.runtime_layout_store, roc_ops); // Get the nominal type information from lhs, or handle anonymous structural types @@ -5961,7 +6033,83 @@ pub const Interpreter = struct { if (slot_ptr.* != 0) { const layout_idx_plus_one = slot_ptr.*; const layout_idx: layout.Idx = @enumFromInt(layout_idx_plus_one - 1); - return self.runtime_layout_store.getLayout(layout_idx); + const cached_layout = self.runtime_layout_store.getLayout(layout_idx); + + // Verify cache is still valid for structure types + // If the type was previously flex (defaulting to Dec) but is now a structure, + // we need to recompute the layout for nominal numeric types + if (resolved.desc.content == .structure) { + const st = resolved.desc.content.structure; + if (st == .nominal_type) { + const nom = st.nominal_type; + const ident_text = self.env.getIdent(nom.ident.ident_idx); + // For nominal numeric types, we can determine the layout directly from the type name + // Check if it's a known numeric type that might have been incorrectly cached as Dec + if (std.mem.eql(u8, ident_text, "I64") or std.mem.eql(u8, ident_text, "Num.I64")) { + const int_layout = layout.Layout.int(types.Int.Precision.i64); + const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); + slot_ptr.* = @intFromEnum(int_layout_idx) + 1; + return int_layout; + } else if (std.mem.eql(u8, ident_text, "I32") or std.mem.eql(u8, ident_text, "Num.I32")) { + const int_layout = layout.Layout.int(types.Int.Precision.i32); + const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); + slot_ptr.* = @intFromEnum(int_layout_idx) + 1; + return int_layout; + } else if (std.mem.eql(u8, ident_text, "I16") or std.mem.eql(u8, ident_text, "Num.I16")) { + const int_layout = layout.Layout.int(types.Int.Precision.i16); + const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); + slot_ptr.* = @intFromEnum(int_layout_idx) + 1; + return int_layout; + } else if (std.mem.eql(u8, ident_text, "I8") or std.mem.eql(u8, ident_text, "Num.I8")) { + const int_layout = layout.Layout.int(types.Int.Precision.i8); + const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); + slot_ptr.* = @intFromEnum(int_layout_idx) + 1; + return int_layout; + } else if (std.mem.eql(u8, ident_text, "I128") or std.mem.eql(u8, ident_text, "Num.I128")) { + const int_layout = layout.Layout.int(types.Int.Precision.i128); + const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); + slot_ptr.* = @intFromEnum(int_layout_idx) + 1; + return int_layout; + } else if (std.mem.eql(u8, ident_text, "U64") or std.mem.eql(u8, ident_text, "Num.U64")) { + const int_layout = layout.Layout.int(types.Int.Precision.u64); + const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); + slot_ptr.* = @intFromEnum(int_layout_idx) + 1; + return int_layout; + } else if (std.mem.eql(u8, ident_text, "U32") or std.mem.eql(u8, ident_text, "Num.U32")) { + const int_layout = layout.Layout.int(types.Int.Precision.u32); + const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); + slot_ptr.* = @intFromEnum(int_layout_idx) + 1; + return int_layout; + } else if (std.mem.eql(u8, ident_text, "U16") or std.mem.eql(u8, ident_text, "Num.U16")) { + const int_layout = layout.Layout.int(types.Int.Precision.u16); + const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); + slot_ptr.* = @intFromEnum(int_layout_idx) + 1; + return int_layout; + } else if (std.mem.eql(u8, ident_text, "U8") or std.mem.eql(u8, ident_text, "Num.U8")) { + const int_layout = layout.Layout.int(types.Int.Precision.u8); + const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); + slot_ptr.* = @intFromEnum(int_layout_idx) + 1; + return int_layout; + } else if (std.mem.eql(u8, ident_text, "U128") or std.mem.eql(u8, ident_text, "Num.U128")) { + const int_layout = layout.Layout.int(types.Int.Precision.u128); + const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); + slot_ptr.* = @intFromEnum(int_layout_idx) + 1; + return int_layout; + } else if (std.mem.eql(u8, ident_text, "F32") or std.mem.eql(u8, ident_text, "Num.F32")) { + const frac_layout = layout.Layout.frac(types.Frac.Precision.f32); + const frac_layout_idx = try self.runtime_layout_store.insertLayout(frac_layout); + slot_ptr.* = @intFromEnum(frac_layout_idx) + 1; + return frac_layout; + } else if (std.mem.eql(u8, ident_text, "F64") or std.mem.eql(u8, ident_text, "Num.F64")) { + const frac_layout = layout.Layout.frac(types.Frac.Precision.f64); + const frac_layout_idx = try self.runtime_layout_store.insertLayout(frac_layout); + slot_ptr.* = @intFromEnum(frac_layout_idx) + 1; + return frac_layout; + } + // For Dec or other nominal types, fall through to use cached value + } + } + return cached_layout; } const layout_idx = switch (resolved.desc.content) { diff --git a/src/eval/test_runner.zig b/src/eval/test_runner.zig index 1dfe626579..a379b25868 100644 --- a/src/eval/test_runner.zig +++ b/src/eval/test_runner.zig @@ -141,11 +141,13 @@ pub const TestRunner = struct { allocator: std.mem.Allocator, cir: *ModuleEnv, builtin_types_param: BuiltinTypes, + other_modules: []const *const can.ModuleEnv, + builtin_module_env: ?*const can.ModuleEnv, ) !TestRunner { return TestRunner{ .allocator = allocator, .env = cir, - .interpreter = try Interpreter.init(allocator, cir, builtin_types_param, null, &[_]*const can.ModuleEnv{}), + .interpreter = try Interpreter.init(allocator, cir, builtin_types_param, builtin_module_env, other_modules), .crash = CrashContext.init(allocator), .roc_ops = null, .test_results = std.array_list.Managed(TestResult).init(allocator), diff --git a/src/playground_wasm/main.zig b/src/playground_wasm/main.zig index df0934b047..c43b87af85 100644 --- a/src/playground_wasm/main.zig +++ b/src/playground_wasm/main.zig @@ -1575,7 +1575,9 @@ fn writeEvaluateTestsResponse(response_buffer: []u8, data: CompilerStageData) Re }; // Create interpreter infrastructure for test evaluation - var test_runner = TestRunner.init(local_arena.allocator(), env, builtin_types_for_tests) catch { + // Note: playground doesn't have access to other modules or builtin module env + const empty_modules: []const *const ModuleEnv = &.{}; + var test_runner = TestRunner.init(local_arena.allocator(), env, builtin_types_for_tests, empty_modules, null) catch { try writeErrorResponse(response_buffer, .ERROR, "Failed to initialize test runner."); return; }; diff --git a/test/fx/expect_with_literal.roc b/test/fx/expect_with_literal.roc new file mode 100644 index 0000000000..c08a364ce7 --- /dev/null +++ b/test/fx/expect_with_literal.roc @@ -0,0 +1,14 @@ +app [main!] { pf: platform "./platform/main.roc" } + +import pf.Stdout +import pf.Stderr + +main! = || { + Stdout.line!("done") +} + +answer : I64 +answer = 42 + +# This tests that numeric literals in expect are properly typed +expect answer == 42 diff --git a/test/fx/expect_with_main.roc b/test/fx/expect_with_main.roc new file mode 100644 index 0000000000..460481b56c --- /dev/null +++ b/test/fx/expect_with_main.roc @@ -0,0 +1,16 @@ +app [main!] { pf: platform "./platform/main.roc" } + +import pf.Stdout +import pf.Stderr + +main! = || { + Stdout.line!("done") +} + +answer : I64 +answer = 42 + +fortytwo : I64 +fortytwo = 42 + +expect answer == fortytwo From d70bc2ae54f035ff3360f920ec0103c137c1f028 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 24 Nov 2025 20:27:46 -0500 Subject: [PATCH 2/8] Remove an unnecessary fallback --- src/eval/interpreter.zig | 78 +--------------------------------------- 1 file changed, 1 insertion(+), 77 deletions(-) diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index a8a6037eef..f8d7bd7f09 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -6033,83 +6033,7 @@ pub const Interpreter = struct { if (slot_ptr.* != 0) { const layout_idx_plus_one = slot_ptr.*; const layout_idx: layout.Idx = @enumFromInt(layout_idx_plus_one - 1); - const cached_layout = self.runtime_layout_store.getLayout(layout_idx); - - // Verify cache is still valid for structure types - // If the type was previously flex (defaulting to Dec) but is now a structure, - // we need to recompute the layout for nominal numeric types - if (resolved.desc.content == .structure) { - const st = resolved.desc.content.structure; - if (st == .nominal_type) { - const nom = st.nominal_type; - const ident_text = self.env.getIdent(nom.ident.ident_idx); - // For nominal numeric types, we can determine the layout directly from the type name - // Check if it's a known numeric type that might have been incorrectly cached as Dec - if (std.mem.eql(u8, ident_text, "I64") or std.mem.eql(u8, ident_text, "Num.I64")) { - const int_layout = layout.Layout.int(types.Int.Precision.i64); - const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); - slot_ptr.* = @intFromEnum(int_layout_idx) + 1; - return int_layout; - } else if (std.mem.eql(u8, ident_text, "I32") or std.mem.eql(u8, ident_text, "Num.I32")) { - const int_layout = layout.Layout.int(types.Int.Precision.i32); - const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); - slot_ptr.* = @intFromEnum(int_layout_idx) + 1; - return int_layout; - } else if (std.mem.eql(u8, ident_text, "I16") or std.mem.eql(u8, ident_text, "Num.I16")) { - const int_layout = layout.Layout.int(types.Int.Precision.i16); - const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); - slot_ptr.* = @intFromEnum(int_layout_idx) + 1; - return int_layout; - } else if (std.mem.eql(u8, ident_text, "I8") or std.mem.eql(u8, ident_text, "Num.I8")) { - const int_layout = layout.Layout.int(types.Int.Precision.i8); - const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); - slot_ptr.* = @intFromEnum(int_layout_idx) + 1; - return int_layout; - } else if (std.mem.eql(u8, ident_text, "I128") or std.mem.eql(u8, ident_text, "Num.I128")) { - const int_layout = layout.Layout.int(types.Int.Precision.i128); - const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); - slot_ptr.* = @intFromEnum(int_layout_idx) + 1; - return int_layout; - } else if (std.mem.eql(u8, ident_text, "U64") or std.mem.eql(u8, ident_text, "Num.U64")) { - const int_layout = layout.Layout.int(types.Int.Precision.u64); - const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); - slot_ptr.* = @intFromEnum(int_layout_idx) + 1; - return int_layout; - } else if (std.mem.eql(u8, ident_text, "U32") or std.mem.eql(u8, ident_text, "Num.U32")) { - const int_layout = layout.Layout.int(types.Int.Precision.u32); - const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); - slot_ptr.* = @intFromEnum(int_layout_idx) + 1; - return int_layout; - } else if (std.mem.eql(u8, ident_text, "U16") or std.mem.eql(u8, ident_text, "Num.U16")) { - const int_layout = layout.Layout.int(types.Int.Precision.u16); - const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); - slot_ptr.* = @intFromEnum(int_layout_idx) + 1; - return int_layout; - } else if (std.mem.eql(u8, ident_text, "U8") or std.mem.eql(u8, ident_text, "Num.U8")) { - const int_layout = layout.Layout.int(types.Int.Precision.u8); - const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); - slot_ptr.* = @intFromEnum(int_layout_idx) + 1; - return int_layout; - } else if (std.mem.eql(u8, ident_text, "U128") or std.mem.eql(u8, ident_text, "Num.U128")) { - const int_layout = layout.Layout.int(types.Int.Precision.u128); - const int_layout_idx = try self.runtime_layout_store.insertLayout(int_layout); - slot_ptr.* = @intFromEnum(int_layout_idx) + 1; - return int_layout; - } else if (std.mem.eql(u8, ident_text, "F32") or std.mem.eql(u8, ident_text, "Num.F32")) { - const frac_layout = layout.Layout.frac(types.Frac.Precision.f32); - const frac_layout_idx = try self.runtime_layout_store.insertLayout(frac_layout); - slot_ptr.* = @intFromEnum(frac_layout_idx) + 1; - return frac_layout; - } else if (std.mem.eql(u8, ident_text, "F64") or std.mem.eql(u8, ident_text, "Num.F64")) { - const frac_layout = layout.Layout.frac(types.Frac.Precision.f64); - const frac_layout_idx = try self.runtime_layout_store.insertLayout(frac_layout); - slot_ptr.* = @intFromEnum(frac_layout_idx) + 1; - return frac_layout; - } - // For Dec or other nominal types, fall through to use cached value - } - } - return cached_layout; + return self.runtime_layout_store.getLayout(layout_idx); } const layout_idx = switch (resolved.desc.content) { From 2b82a9965c828c506642eacb1839461cfa04716e Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 24 Nov 2025 20:32:49 -0500 Subject: [PATCH 3/8] Give playground access to builtin modules --- src/playground_wasm/main.zig | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/playground_wasm/main.zig b/src/playground_wasm/main.zig index c43b87af85..cb590a28c8 100644 --- a/src/playground_wasm/main.zig +++ b/src/playground_wasm/main.zig @@ -133,6 +133,7 @@ const CompilerStageData = struct { solver: ?Check = null, bool_stmt: ?can.CIR.Statement.Idx = null, builtin_types: ?eval.BuiltinTypes = null, + builtin_module: ?BuiltinModule = null, // Pre-canonicalization HTML representations tokens_html: ?[]const u8 = null, @@ -145,6 +146,18 @@ const CompilerStageData = struct { can_reports: std.array_list.Managed(reporting.Report), type_reports: std.array_list.Managed(reporting.Report), + const BuiltinModule = struct { + env: *ModuleEnv, + buffer: []align(collections.CompactWriter.SERIALIZATION_ALIGNMENT.toByteUnits()) u8, + gpa: Allocator, + + fn deinit(self: *@This()) void { + self.env.imports.map.deinit(self.gpa); + self.gpa.free(self.buffer); + self.gpa.destroy(self.env); + } + }; + pub fn init(alloc: Allocator, module_env: *ModuleEnv) CompilerStageData { return CompilerStageData{ .module_env = module_env, @@ -195,6 +208,11 @@ const CompilerStageData = struct { // Finally, deinit the ModuleEnv and free its memory self.module_env.deinit(); allocator.destroy(self.module_env); + + // Deinit the builtin module if it was loaded + if (self.builtin_module) |*bm| { + bm.deinit(); + } } }; @@ -1001,8 +1019,13 @@ fn compileSource(source: []const u8) !CompilerStageData { logDebug("compileSource: Loading Builtin module\n", .{}); const builtin_source = compiled_builtins.builtin_source; - var builtin_module = try LoadedModule.loadCompiledModule(allocator, compiled_builtins.builtin_bin, "Builtin", builtin_source); - defer builtin_module.deinit(); + const builtin_module = try LoadedModule.loadCompiledModule(allocator, compiled_builtins.builtin_bin, "Builtin", builtin_source); + // Store in result instead of deferring deinit - we need it for test evaluation + result.builtin_module = .{ + .env = builtin_module.env, + .buffer = builtin_module.buffer, + .gpa = builtin_module.gpa, + }; logDebug("compileSource: Builtin module loaded\n", .{}); // Get builtin statement indices from the builtin module @@ -1575,9 +1598,9 @@ fn writeEvaluateTestsResponse(response_buffer: []u8, data: CompilerStageData) Re }; // Create interpreter infrastructure for test evaluation - // Note: playground doesn't have access to other modules or builtin module env const empty_modules: []const *const ModuleEnv = &.{}; - var test_runner = TestRunner.init(local_arena.allocator(), env, builtin_types_for_tests, empty_modules, null) catch { + const builtin_module_env: ?*const ModuleEnv = if (data.builtin_module) |bm| bm.env else null; + var test_runner = TestRunner.init(local_arena.allocator(), env, builtin_types_for_tests, empty_modules, builtin_module_env) catch { try writeErrorResponse(response_buffer, .ERROR, "Failed to initialize test runner."); return; }; From 3128c69057077b2ff781d48aa46b0c584920587a Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 24 Nov 2025 20:49:20 -0500 Subject: [PATCH 4/8] Write a test to verify an assumption --- src/check/test/num_type_inference_test.zig | 59 ++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/check/test/num_type_inference_test.zig b/src/check/test/num_type_inference_test.zig index 8c8420eb0e..5ec69a9f75 100644 --- a/src/check/test/num_type_inference_test.zig +++ b/src/check/test/num_type_inference_test.zig @@ -133,3 +133,62 @@ test "infers type for octal literals" { try test_env.assertLastDefTypeContains("from_numeral"); } } + +test "numeric literal in comparison unifies with typed operand" { + // When comparing a typed variable with a numeric literal, + // the literal should unify to match the variable's type. + // `answer == 42` desugars to `answer.is_eq(42)`, which dispatches to I64.is_eq(answer, 42), + // which should unify 42's flex var with I64. + const source = + \\answer : I64 + \\answer = 42 + \\ + \\result = answer == 42 + ; + + var test_env = try TestEnv.init("Test", source); + defer test_env.deinit(); + + // First verify no type errors + try test_env.assertNoErrors(); + + // Verify that `answer` has type I64 + try test_env.assertDefType("answer", "I64"); + + // Verify that `result` has type Bool (the result of ==) + try test_env.assertDefType("result", "Bool"); + + // Now verify that the binop expression's operands both have I64 type + // Find the `result` definition and check the binop's operand types + const ModuleEnv = @import("can").ModuleEnv; + const defs_slice = test_env.module_env.store.sliceDefs(test_env.module_env.all_defs); + var found_result = false; + for (defs_slice) |def_idx| { + const def = test_env.module_env.store.getDef(def_idx); + const ptrn = test_env.module_env.store.getPattern(def.pattern); + if (ptrn == .assign) { + const def_name = test_env.module_env.getIdentStoreConst().getText(ptrn.assign.ident); + if (std.mem.eql(u8, def_name, "result")) { + found_result = true; + // Get the expression - should be a binop + const expr = test_env.module_env.store.getExpr(def.expr); + try testing.expect(expr == .e_binop); + const binop = expr.e_binop; + + // Check LHS type (should be I64) + const lhs_var = ModuleEnv.varFrom(binop.lhs); + try test_env.type_writer.write(lhs_var); + const lhs_type = test_env.type_writer.get(); + try testing.expectEqualStrings("I64", lhs_type); + + // Check RHS type (the literal 42 - should also be I64 after unification) + const rhs_var = ModuleEnv.varFrom(binop.rhs); + try test_env.type_writer.write(rhs_var); + const rhs_type = test_env.type_writer.get(); + try testing.expectEqualStrings("I64", rhs_type); + break; + } + } + } + try testing.expect(found_result); +} From 3bdaef97976bee05eebab4f8624cd6937fd85593 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 24 Nov 2025 21:14:33 -0500 Subject: [PATCH 5/8] Fix `expect` not getting type-checked --- src/check/Check.zig | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/check/Check.zig b/src/check/Check.zig index dcd4fafd9e..1a3384f878 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -1022,6 +1022,43 @@ pub fn checkFile(self: *Self) std.mem.Allocator.Error!void { try self.checkDef(def_idx, &env); } + // Finally, type-check top-level statements (like expect) + // These are separate from defs and need to be checked after all defs are processed + // so that lookups can find their definitions + for (stmts_slice) |stmt_idx| { + const stmt = self.cir.store.getStatement(stmt_idx); + const stmt_var = ModuleEnv.varFrom(stmt_idx); + const stmt_region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(stmt_idx)); + + switch (stmt) { + .s_expect => |expr_stmt| { + env.reset(); + + // Enter a new rank for this expect + try env.var_pool.pushRank(); + defer env.var_pool.popRank(); + + // Check the body expression + _ = try self.checkExpr(expr_stmt.body, &env, .no_expectation); + const body_var: Var = ModuleEnv.varFrom(expr_stmt.body); + + // Unify with Bool (expects must be bool expressions) + const bool_var = try self.freshBool(&env, stmt_region); + _ = try self.unify(bool_var, body_var, &env); + + // Unify statement var with body var + _ = try self.unify(stmt_var, body_var, &env); + + // Generalize and check deferred constraints + try self.generalizer.generalize(self.gpa, &env.var_pool, env.rank()); + try self.checkDeferredStaticDispatchConstraints(&env); + }, + else => { + // Other statement types are handled elsewhere (type decls, defs, etc.) + }, + } + } + // Note that we can't use SCCs to determine the order to resolve defs // because anonymous static dispatch makes function order not knowable // before type inference From a7dc202d9ad4d58648691ac0284af262bb90caaf Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 24 Nov 2025 21:14:42 -0500 Subject: [PATCH 6/8] Improve a test --- src/check/test/num_type_inference_test.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/check/test/num_type_inference_test.zig b/src/check/test/num_type_inference_test.zig index 5ec69a9f75..431c01b2de 100644 --- a/src/check/test/num_type_inference_test.zig +++ b/src/check/test/num_type_inference_test.zig @@ -186,6 +186,14 @@ test "numeric literal in comparison unifies with typed operand" { try test_env.type_writer.write(rhs_var); const rhs_type = test_env.type_writer.get(); try testing.expectEqualStrings("I64", rhs_type); + + // Verify that the RHS type var is actually resolved to a nominal type, not flex + // This is what the interpreter's translateTypeVar should see + const rhs_resolved = test_env.module_env.types.resolveVar(rhs_var); + // After type checking, the RHS (numeric literal) should be unified to I64, + // which is a nominal type (structure.nominal_type), NOT a flex var + try testing.expect(rhs_resolved.desc.content == .structure); + try testing.expect(rhs_resolved.desc.content.structure == .nominal_type); break; } } From c044e6870b3ee6b38b39a5a8ac5566bf65a51825 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 24 Nov 2025 21:52:41 -0500 Subject: [PATCH 7/8] Remove a workaround --- src/eval/interpreter.zig | 76 ++-------------------------------------- 1 file changed, 3 insertions(+), 73 deletions(-) diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index f8d7bd7f09..61dc6f765a 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -5565,59 +5565,6 @@ pub const Interpreter = struct { return error.MethodNotFound; } - /// Evaluate a numeric literal expression with a specific layout. - /// This is used when we know the target type from context (e.g., comparing with a typed value). - fn evalNumLitWithLayout( - self: *Interpreter, - expr_idx: can.CIR.Expr.Idx, - target_layout: layout.Layout, - roc_ops: *RocOps, - ) Error!StackValue { - _ = roc_ops; - const expr = self.env.store.getExpr(expr_idx); - if (expr != .e_num) { - return error.TypeMismatch; - } - const num_lit = expr.e_num; - - var value = try self.pushRaw(target_layout, 0); - value.is_initialized = false; - - switch (target_layout.tag) { - .scalar => switch (target_layout.data.scalar.tag) { - .int => try value.setIntFromBytes(num_lit.value.bytes, num_lit.value.kind == .u128), - .frac => switch (target_layout.data.scalar.data.frac) { - .f32 => { - const ptr = @as(*f32, @ptrCast(@alignCast(value.ptr.?))); - if (num_lit.value.kind == .u128) { - const u128_val: u128 = @bitCast(num_lit.value.bytes); - ptr.* = @floatFromInt(u128_val); - } else { - ptr.* = @floatFromInt(num_lit.value.toI128()); - } - }, - .f64 => { - const ptr = @as(*f64, @ptrCast(@alignCast(value.ptr.?))); - if (num_lit.value.kind == .u128) { - const u128_val: u128 = @bitCast(num_lit.value.bytes); - ptr.* = @floatFromInt(u128_val); - } else { - ptr.* = @floatFromInt(num_lit.value.toI128()); - } - }, - .dec => { - const ptr = @as(*RocDec, @ptrCast(@alignCast(value.ptr.?))); - ptr.* = .{ .num = num_lit.value.toI128() * RocDec.one_point_zero_i128 }; - }, - }, - else => return error.TypeMismatch, - }, - else => return error.TypeMismatch, - } - value.is_initialized = true; - return value; - } - /// Dispatch a binary operator to its corresponding method. /// Handles the full method dispatch including: /// - Type resolution with Dec default for flex/rigid vars @@ -5633,6 +5580,7 @@ pub const Interpreter = struct { ) Error!StackValue { const lhs_ct_var = can.ModuleEnv.varFrom(lhs_expr); const lhs_rt_var = try self.translateTypeVar(self.env, lhs_ct_var); + const rhs_ct_var = can.ModuleEnv.varFrom(rhs_expr); const rhs_rt_var = try self.translateTypeVar(self.env, rhs_ct_var); @@ -5648,29 +5596,11 @@ pub const Interpreter = struct { lhs_resolved = self.runtime_types.resolveVar(dec_var); } - // Evaluate LHS first + // Evaluate both operands var lhs = try self.evalExprMinimal(lhs_expr, roc_ops, lhs_rt_var); defer lhs.decref(&self.runtime_layout_store, roc_ops); - // For numeric binary operations, if LHS has a scalar layout (numeric) and RHS is a flex/rigid var, - // we need to use the LHS's actual layout to determine RHS type - const rhs_resolved = self.runtime_types.resolveVar(rhs_rt_var); - const rhs_expr_data = self.env.store.getExpr(rhs_expr); - - // Check if RHS is a numeric literal that needs to be coerced to match LHS - const rhs_is_flex = rhs_resolved.desc.content == .flex or rhs_resolved.desc.content == .rigid; - const lhs_is_numeric = lhs.layout.tag == .scalar and - (lhs.layout.data.scalar.tag == .int or lhs.layout.data.scalar.tag == .frac); - const should_coerce_rhs = rhs_is_flex and lhs_is_numeric and rhs_expr_data == .e_num; - - // Evaluate RHS with LHS's layout if coercion is needed - var rhs: StackValue = undefined; - if (should_coerce_rhs) { - // Evaluate the numeric literal using the LHS's layout type - rhs = try self.evalNumLitWithLayout(rhs_expr, lhs.layout, roc_ops); - } else { - rhs = try self.evalExprMinimal(rhs_expr, roc_ops, rhs_rt_var); - } + var rhs = try self.evalExprMinimal(rhs_expr, roc_ops, rhs_rt_var); defer rhs.decref(&self.runtime_layout_store, roc_ops); // Get the nominal type information from lhs, or handle anonymous structural types From d5e38a458ba7c629026a761e1311b2e96f189208 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 24 Nov 2025 22:19:36 -0500 Subject: [PATCH 8/8] Update tests --- test/snapshots/fuzz_crash/fuzz_crash_019.md | 12 +++++++++++ test/snapshots/fuzz_crash/fuzz_crash_020.md | 12 +++++++++++ test/snapshots/plume_package/Color.md | 24 +++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/test/snapshots/fuzz_crash/fuzz_crash_019.md b/test/snapshots/fuzz_crash/fuzz_crash_019.md index 30e790a180..e9def4620b 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_019.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_019.md @@ -197,6 +197,7 @@ UNUSED VALUE - fuzz_crash_019.md:86:11:86:17 UNUSED VALUE - fuzz_crash_019.md:98:4:104:3 UNUSED VALUE - fuzz_crash_019.md:105:2:105:54 UNUSED VALUE - fuzz_crash_019.md:105:55:105:85 +UNUSED VALUE - fuzz_crash_019.md:119:2:119:10 # PROBLEMS **PARSE ERROR** A parsing error occurred: `match_branch_missing_arrow` @@ -998,6 +999,17 @@ This expression produces a value, but it's not being used: It has the type: __f_ +**UNUSED VALUE** +This expression produces a value, but it's not being used: +**fuzz_crash_019.md:119:2:119:10:** +```roc + foo == 1 +``` + ^^^^^^^^ + +It has the type: + _Bool_ + # TOKENS ~~~zig KwApp,OpenSquare,LowerIdent,CloseSquare,OpenCurly,LowerIdent,OpColon,KwPlatform,StringStart,StringPart,StringEnd,CloseCurly, diff --git a/test/snapshots/fuzz_crash/fuzz_crash_020.md b/test/snapshots/fuzz_crash/fuzz_crash_020.md index a85c5836e7..5222b2e438 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_020.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_020.md @@ -196,6 +196,7 @@ UNUSED VALUE - fuzz_crash_020.md:86:11:86:17 UNUSED VALUE - fuzz_crash_020.md:98:4:104:3 UNUSED VALUE - fuzz_crash_020.md:105:2:105:54 UNUSED VALUE - fuzz_crash_020.md:105:55:105:85 +UNUSED VALUE - fuzz_crash_020.md:119:2:119:10 # PROBLEMS **PARSE ERROR** A parsing error occurred: `match_branch_missing_arrow` @@ -981,6 +982,17 @@ This expression produces a value, but it's not being used: It has the type: __f_ +**UNUSED VALUE** +This expression produces a value, but it's not being used: +**fuzz_crash_020.md:119:2:119:10:** +```roc + foo == 1 +``` + ^^^^^^^^ + +It has the type: + _Bool_ + # TOKENS ~~~zig KwApp,OpenSquare,LowerIdent,CloseSquare,OpenCurly,LowerIdent,OpColon,KwPlatform,StringStart,StringPart,StringEnd,CloseCurly, diff --git a/test/snapshots/plume_package/Color.md b/test/snapshots/plume_package/Color.md index 35af210c1c..00a1997700 100644 --- a/test/snapshots/plume_package/Color.md +++ b/test/snapshots/plume_package/Color.md @@ -92,6 +92,8 @@ MISSING METHOD - Color.md:22:15:22:26 MISSING METHOD - Color.md:29:13:29:26 TYPE MISMATCH - Color.md:32:5:45:6 MISSING METHOD - Color.md:62:8:62:28 +MISSING METHOD - Color.md:56:8:56:34 +MISSING METHOD - Color.md:57:8:57:40 # PROBLEMS **MODULE HEADER DEPRECATED** The `module` header is deprecated. @@ -266,6 +268,28 @@ This **is_named_color** method is being called on the type **Str**, which has no **Hint: **For this to work, the type would need to have a method named **is_named_color** associated with it in the type's declaration. +**MISSING METHOD** +This **to_str** method is being called on the type **Color**, which has no method with that name: +**Color.md:56:8:56:34:** +```roc +expect rgb(124, 56, 245).to_str() == "rgb(124, 56, 245)" +``` + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + +**Hint: **For this to work, the type would need to have a method named **to_str** associated with it in the type's declaration. + +**MISSING METHOD** +This **to_str** method is being called on the type **Color**, which has no method with that name: +**Color.md:57:8:57:40:** +```roc +expect rgba(124, 56, 245, 255).to_str() == "rgba(124, 56, 245, 1.0)" +``` + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + +**Hint: **For this to work, the type would need to have a method named **to_str** associated with it in the type's declaration. + # TOKENS ~~~zig KwModule,OpenSquare,