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 diff --git a/src/check/test/num_type_inference_test.zig b/src/check/test/num_type_inference_test.zig index 8c8420eb0e..431c01b2de 100644 --- a/src/check/test/num_type_inference_test.zig +++ b/src/check/test/num_type_inference_test.zig @@ -133,3 +133,70 @@ 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); + + // 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; + } + } + } + try testing.expect(found_result); +} 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 f50e1f95a0..1cc9259369 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -5603,6 +5603,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); @@ -5621,6 +5622,7 @@ pub const Interpreter = struct { // Evaluate both operands 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); defer rhs.decref(&self.runtime_layout_store, roc_ops); 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..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,7 +1598,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 { + const empty_modules: []const *const ModuleEnv = &.{}; + 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; }; 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 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,