diff --git a/.github/workflows/ci_manager.yml b/.github/workflows/ci_manager.yml index 49a2301720..6183839cfe 100644 --- a/.github/workflows/ci_manager.yml +++ b/.github/workflows/ci_manager.yml @@ -35,6 +35,7 @@ jobs: - '.github/actions/flaky-retry/action.yml' - 'ci/zig_lints.sh' - 'ci/check_test_wiring.zig' + - 'ci/valgrind_clean.sh' - uses: dorny/paths-filter@v3 id: other_filter with: @@ -53,6 +54,7 @@ jobs: - '!.github/actions/flaky-retry/action.yml' - '!ci/zig_lints.sh' - '!ci/check_test_wiring.zig' + - '!ci/valgrind_clean.sh' # Files that ci manager workflows should not run on. - '!.gitignore' - '!.reuse' diff --git a/.github/workflows/ci_zig.yml b/.github/workflows/ci_zig.yml index f5dee1f4d6..04d37b6ee1 100644 --- a/.github/workflows/ci_zig.yml +++ b/.github/workflows/ci_zig.yml @@ -194,9 +194,9 @@ jobs: # We can re-evaluate as new version of zig/valgrind come out. if: ${{ matrix.os == 'ubuntu-22.04' }} run: | - sudo apt install -y valgrind + sudo snap install valgrind --classic valgrind --version - valgrind --leak-check=full --error-exitcode=1 --errors-for-leak-kinds=definite,possible ./zig-out/bin/snapshot --debug + ./ci/valgind_clean.sh --leak-check=full --error-exitcode=1 --errors-for-leak-kinds=definite,possible ./zig-out/bin/snapshot --debug - name: check if statically linked (ubuntu) if: startsWith(matrix.os, 'ubuntu') diff --git a/build.zig b/build.zig index 57fb74fa51..da9dbf0d8b 100644 --- a/build.zig +++ b/build.zig @@ -242,7 +242,17 @@ const CheckTypeCheckerPatternsStep = struct { std.mem.startsWith(u8, after_match, "order") or std.mem.startsWith(u8, after_match, "copyForwards"); - if (!is_allowed) { + // Also allow module name comparisons - these are legitimately string-based + // (matching import names to loaded modules at runtime) + const is_module_name_comparison = + std.mem.indexOf(u8, line, "module_name") != null or + std.mem.indexOf(u8, line, "import_name") != null or + std.mem.indexOf(u8, line, "qualified_name") != null or + std.mem.indexOf(u8, line, "type_name") != null or + std.mem.indexOf(u8, line, "type_path") != null or + std.mem.indexOf(u8, line, "origin_env") != null; + + if (!is_allowed and !is_module_name_comparison) { try violations.append(allocator, .{ .file_path = full_path, .line_number = line_number, diff --git a/ci/valgind_clean.sh b/ci/valgind_clean.sh new file mode 100755 index 0000000000..393d908adc --- /dev/null +++ b/ci/valgind_clean.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +valgrind "$@" 2>&1 | grep -v "Warning: DWARF2 reader: Badly formed extended line op encountered" +exit ${PIPESTATUS[0]} diff --git a/src/canonicalize/ModuleEnv.zig b/src/canonicalize/ModuleEnv.zig index 240995dce6..8d1a6d8c3d 100644 --- a/src/canonicalize/ModuleEnv.zig +++ b/src/canonicalize/ModuleEnv.zig @@ -179,6 +179,8 @@ is_ne_ident: Ident.Idx, // These match the nominal types created during type checking builtin_try_ident: Ident.Idx, builtin_numeral_ident: Ident.Idx, +/// Relative numeral ident "Num.Numeral" - used for creating nominal types where origin_module is Builtin +numeral_relative_ident: Ident.Idx, builtin_str_ident: Ident.Idx, list_type_ident: Ident.Idx, box_type_ident: Ident.Idx, @@ -211,6 +213,9 @@ unbox_method_ident: Ident.Idx, // Try tag idents ok_ident: Ident.Idx, err_ident: Ident.Idx, +// Bool tag idents +true_tag_ident: Ident.Idx, +false_tag_ident: Ident.Idx, /// Deferred numeric literals collected during type checking /// These will be validated during comptime evaluation @@ -317,6 +322,7 @@ pub fn init(gpa: std.mem.Allocator, source: []const u8) std.mem.Allocator.Error! // Pre-intern fully-qualified type identifiers for type checking and layout generation const builtin_try_ident_val = try common.insertIdent(gpa, Ident.for_text("Builtin.Try")); const builtin_numeral_ident_val = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.Numeral")); + const numeral_relative_ident = try common.insertIdent(gpa, Ident.for_text("Num.Numeral")); const builtin_str_ident = try common.insertIdent(gpa, Ident.for_text("Builtin.Str")); const list_type_ident = try common.insertIdent(gpa, Ident.for_text("List")); const box_type_ident = try common.insertIdent(gpa, Ident.for_text("Box")); @@ -345,6 +351,8 @@ pub fn init(gpa: std.mem.Allocator, source: []const u8) std.mem.Allocator.Error! const unbox_method_ident = try common.insertIdent(gpa, Ident.for_text("unbox")); const ok_ident = try common.insertIdent(gpa, Ident.for_text("Ok")); const err_ident = try common.insertIdent(gpa, Ident.for_text("Err")); + const true_tag_ident = try common.insertIdent(gpa, Ident.for_text("True")); + const false_tag_ident = try common.insertIdent(gpa, Ident.for_text("False")); return Self{ .gpa = gpa, @@ -384,6 +392,7 @@ pub fn init(gpa: std.mem.Allocator, source: []const u8) std.mem.Allocator.Error! .is_ne_ident = is_ne_ident, .builtin_try_ident = builtin_try_ident_val, .builtin_numeral_ident = builtin_numeral_ident_val, + .numeral_relative_ident = numeral_relative_ident, .builtin_str_ident = builtin_str_ident, .list_type_ident = list_type_ident, .box_type_ident = box_type_ident, @@ -412,6 +421,8 @@ pub fn init(gpa: std.mem.Allocator, source: []const u8) std.mem.Allocator.Error! .unbox_method_ident = unbox_method_ident, .ok_ident = ok_ident, .err_ident = err_ident, + .true_tag_ident = true_tag_ident, + .false_tag_ident = false_tag_ident, .deferred_numeric_literals = try DeferredNumericLiteral.SafeList.initCapacity(gpa, 32), .import_mapping = types_mod.import_mapping.ImportMapping.init(gpa), }; @@ -1858,6 +1869,7 @@ pub const Serialized = extern struct { // Fully-qualified type identifiers for type checking and layout generation builtin_try_ident: Ident.Idx, builtin_numeral_ident: Ident.Idx, + numeral_relative_ident: Ident.Idx, builtin_str_ident: Ident.Idx, list_type_ident: Ident.Idx, box_type_ident: Ident.Idx, @@ -1886,6 +1898,8 @@ pub const Serialized = extern struct { unbox_method_ident: Ident.Idx, ok_ident: Ident.Idx, err_ident: Ident.Idx, + true_tag_ident: Ident.Idx, + false_tag_ident: Ident.Idx, deferred_numeric_literals: DeferredNumericLiteral.SafeList.Serialized, import_mapping_reserved: [6]u64, // Reserved space for import_mapping (AutoHashMap is ~40 bytes), initialized at runtime @@ -1947,6 +1961,7 @@ pub const Serialized = extern struct { self.is_ne_ident = env.is_ne_ident; self.builtin_try_ident = env.builtin_try_ident; self.builtin_numeral_ident = env.builtin_numeral_ident; + self.numeral_relative_ident = env.numeral_relative_ident; self.builtin_str_ident = env.builtin_str_ident; self.list_type_ident = env.list_type_ident; self.box_type_ident = env.box_type_ident; @@ -1975,6 +1990,8 @@ pub const Serialized = extern struct { self.unbox_method_ident = env.unbox_method_ident; self.ok_ident = env.ok_ident; self.err_ident = env.err_ident; + self.true_tag_ident = env.true_tag_ident; + self.false_tag_ident = env.false_tag_ident; // import_mapping is runtime-only and initialized fresh during deserialization self.import_mapping_reserved = .{ 0, 0, 0, 0, 0, 0 }; } @@ -2043,6 +2060,7 @@ pub const Serialized = extern struct { // Fully-qualified type identifiers for type checking and layout generation .builtin_try_ident = self.builtin_try_ident, .builtin_numeral_ident = self.builtin_numeral_ident, + .numeral_relative_ident = self.numeral_relative_ident, .builtin_str_ident = self.builtin_str_ident, .list_type_ident = self.list_type_ident, .box_type_ident = self.box_type_ident, @@ -2071,6 +2089,8 @@ pub const Serialized = extern struct { .unbox_method_ident = self.unbox_method_ident, .ok_ident = self.ok_ident, .err_ident = self.err_ident, + .true_tag_ident = self.true_tag_ident, + .false_tag_ident = self.false_tag_ident, .deferred_numeric_literals = self.deferred_numeric_literals.deserialize(offset).*, .import_mapping = types_mod.import_mapping.ImportMapping.init(gpa), }; diff --git a/src/check/Check.zig b/src/check/Check.zig index 5a59e69eba..f35f2f4bfe 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -855,12 +855,13 @@ fn mkNumeralContent(self: *Self, env: *Env) Allocator.Error!Content { else self.common_idents.module_name; // We're compiling Builtin module itself - // Use the relative name "Num.Numeral" (not "Builtin.Num.Numeral") to match the relative_name in TypeHeader - // The origin_module field already captures that this type is from Builtin - const numeral_ident_idx = self.cir.common.findIdent("Num.Numeral") orelse blk: { - // If not found, create it (this handles tests and edge cases) - break :blk try @constCast(self.cir).insertIdent(base.Ident.for_text("Num.Numeral")); - }; + // Use the relative name "Num.Numeral" (not "Builtin.Num.Numeral") with origin_module Builtin + // Use the pre-interned ident from builtin_module to avoid string comparison + const numeral_ident_idx = if (self.common_idents.builtin_module) |builtin_mod| + builtin_mod.numeral_relative_ident + else + // When compiling Builtin module itself, use the pre-interned ident from self.cir + self.cir.numeral_relative_ident; const numeral_ident = types_mod.TypeIdent{ .ident_idx = numeral_ident_idx, }; @@ -3563,8 +3564,10 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) try self.unifyWith(expr_var, .{ .flex = Flex.init() }, env); }, .e_dbg => |dbg| { - does_fx = try self.checkExpr(dbg.expr, env, expected) or does_fx; - _ = try self.unify(expr_var, ModuleEnv.varFrom(dbg.expr), env); + // dbg evaluates its inner expression but returns {} (like expect) + _ = try self.checkExpr(dbg.expr, env, .no_expectation); + does_fx = true; + try self.unifyWith(expr_var, .{ .structure = .empty_record }, env); }, .e_expect => |expect| { does_fx = try self.checkExpr(expect.body, env, expected) or does_fx; @@ -5146,10 +5149,10 @@ fn checkDeferredStaticDispatchConstraints(self: *Self, env: *Env) std.mem.Alloca const constraints = self.types.sliceStaticDispatchConstraints(deferred_constraint.constraints); if (constraints.len > 0) { const constraint = constraints[0]; - const constraint_fn_name_bytes = self.cir.getIdent(constraint.fn_name); // For is_eq constraints, use the specific equality error message - if (std.mem.eql(u8, constraint_fn_name_bytes, "is_eq")) { + // Use ident index comparison instead of string comparison + if (constraint.fn_name == self.cir.is_eq_ident) { try self.reportEqualityError( deferred_constraint.var_, constraint, diff --git a/src/check/test/TestEnv.zig b/src/check/test/TestEnv.zig index 98281469b7..3427d81f09 100644 --- a/src/check/test/TestEnv.zig +++ b/src/check/test/TestEnv.zig @@ -136,6 +136,9 @@ fn loadCompiledModule(gpa: std.mem.Allocator, bin_data: []const u8, module_name: .unbox_method_ident = common.findIdent("unbox") orelse unreachable, .ok_ident = common.findIdent("Ok") orelse unreachable, .err_ident = common.findIdent("Err") orelse unreachable, + .numeral_relative_ident = common.findIdent("Num.Numeral") orelse unreachable, + .true_tag_ident = common.findIdent("True") orelse unreachable, + .false_tag_ident = common.findIdent("False") orelse unreachable, .deferred_numeric_literals = try ModuleEnv.DeferredNumericLiteral.SafeList.initCapacity(gpa, 0), .import_mapping = types.import_mapping.ImportMapping.init(gpa), }; diff --git a/src/check/test/type_checking_integration.zig b/src/check/test/type_checking_integration.zig index 71f558bbd2..414d8a6c8b 100644 --- a/src/check/test/type_checking_integration.zig +++ b/src/check/test/type_checking_integration.zig @@ -1381,13 +1381,16 @@ test "check type - crash" { ); } -// debug // +// dbg // -test "check type - debug" { +test "check type - dbg" { + // dbg returns {} (not the value it's debugging), so it can be used + // as a statement/side-effect without affecting the block's return type const source = \\y : U64 \\y = { - \\ debug 2 + \\ dbg 2 + \\ 42 \\} \\ \\main = { diff --git a/src/cli/test/fx_platform_test.zig b/src/cli/test/fx_platform_test.zig index 39a65160d2..225d16d061 100644 --- a/src/cli/test/fx_platform_test.zig +++ b/src/cli/test/fx_platform_test.zig @@ -340,6 +340,46 @@ test "fx platform match with wildcard" { } } +test "fx platform dbg missing return value" { + const allocator = testing.allocator; + + try ensureRocBinary(allocator); + + // Run an app that uses dbg as the last expression in main!. + // dbg is treated as a statement (side-effect only) when it's the final + // expression in a block, so the block returns {} as expected by main!. + const run_result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + "./zig-out/bin/roc", + "--no-cache", + "test/fx/dbg_missing_return.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; + }, + } + + // Verify that the dbg output was printed + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "this should work now") != null); +} + test "fx platform check unused state var reports correct errors" { const allocator = testing.allocator; diff --git a/src/compile/test/module_env_test.zig b/src/compile/test/module_env_test.zig index 1acb5d50e9..5b708b2bf2 100644 --- a/src/compile/test/module_env_test.zig +++ b/src/compile/test/module_env_test.zig @@ -153,6 +153,9 @@ test "ModuleEnv.Serialized roundtrip" { .unbox_method_ident = common.findIdent("unbox") orelse unreachable, .ok_ident = common.findIdent("Ok") orelse unreachable, .err_ident = common.findIdent("Err") orelse unreachable, + .numeral_relative_ident = common.findIdent("Num.Numeral") orelse unreachable, + .true_tag_ident = common.findIdent("True") orelse unreachable, + .false_tag_ident = common.findIdent("False") orelse unreachable, .deferred_numeric_literals = try ModuleEnv.DeferredNumericLiteral.SafeList.initCapacity(deser_alloc, 0), .import_mapping = types.import_mapping.ImportMapping.init(deser_alloc), }; @@ -167,7 +170,8 @@ test "ModuleEnv.Serialized roundtrip" { // Plus 3 field/tag identifiers: before_dot, after_dot, ProvidedByCompiler // Plus 7 more identifiers: tag, payload, is_negative, digits_before_pt, digits_after_pt, box, unbox // Plus 2 Try tag identifiers: Ok, Err - try testing.expectEqual(@as(u32, 52), original.common.idents.interner.entry_count); + // Plus 3 more identifiers: Num.Numeral (relative), True, False + try testing.expectEqual(@as(u32, 55), original.common.idents.interner.entry_count); try testing.expectEqualStrings("hello", original.getIdent(hello_idx)); try testing.expectEqualStrings("world", original.getIdent(world_idx)); @@ -176,8 +180,8 @@ test "ModuleEnv.Serialized roundtrip" { try testing.expectEqual(@as(usize, 2), original.imports.imports.len()); // Should have 2 unique imports // First verify that the CommonEnv data was preserved after deserialization - // Should have same 52 identifiers as original: hello, world, TestModule + 19 well-known identifiers + 18 type identifiers + 3 field/tag identifiers + 7 more identifiers + 2 Try tag identifiers from ModuleEnv.init() - try testing.expectEqual(@as(u32, 52), env.common.idents.interner.entry_count); + // Should have same 55 identifiers as original: hello, world, TestModule + 19 well-known identifiers + 18 type identifiers + 3 field/tag identifiers + 7 more identifiers + 2 Try tag identifiers + 3 additional (Num.Numeral relative, True, False) from ModuleEnv.init() + try testing.expectEqual(@as(u32, 55), env.common.idents.interner.entry_count); try testing.expectEqual(@as(usize, 1), env.common.exposed_items.count()); try testing.expectEqual(@as(?u16, 42), env.common.exposed_items.getNodeIndexById(gpa, @as(u32, @bitCast(hello_idx)))); diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index 78c9682f6f..e66c87e5c3 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -460,7 +460,7 @@ pub const Interpreter = struct { defer result_value.decref(&self.runtime_layout_store, roc_ops); // Only copy result if the result type is compatible with ret_ptr - if (try self.shouldCopyResult(result_value, ret_ptr)) { + if (try self.shouldCopyResult(result_value, ret_ptr, roc_ops)) { try result_value.copyToPtr(&self.runtime_layout_store, ret_ptr, roc_ops); } return; @@ -470,7 +470,7 @@ pub const Interpreter = struct { defer result.decref(&self.runtime_layout_store, roc_ops); // Only copy result if the result type is compatible with ret_ptr - if (try self.shouldCopyResult(result, ret_ptr)) { + if (try self.shouldCopyResult(result, ret_ptr, roc_ops)) { try result.copyToPtr(&self.runtime_layout_store, ret_ptr, roc_ops); } } @@ -478,7 +478,7 @@ pub const Interpreter = struct { /// Check if the result should be copied to ret_ptr based on the result's layout. /// Returns false for zero-sized types (nothing to copy). /// Validates that ret_ptr is properly aligned for the result type. - fn shouldCopyResult(self: *Interpreter, result: StackValue, ret_ptr: *anyopaque) !bool { + fn shouldCopyResult(self: *Interpreter, result: StackValue, ret_ptr: *anyopaque, _: *RocOps) !bool { const result_size = self.runtime_layout_store.layoutSize(result.layout); if (result_size == 0) { // Zero-sized types don't need copying @@ -493,7 +493,6 @@ pub const Interpreter = struct { const required_alignment = result.layout.alignment(self.runtime_layout_store.targetUsize()); const ret_addr = @intFromPtr(ret_ptr); if (ret_addr % required_alignment.toByteUnits() != 0) { - // Type mismatch detected at runtime return error.TypeMismatch; } @@ -1363,9 +1362,9 @@ pub const Interpreter = struct { }; var resolved = self.runtime_types.resolveVar(rt_var); // If the type is still flex and this is a True/False tag, use Bool + // Use ident index comparison instead of string comparison if (resolved.desc.content == .flex) { - const name_text = self.env.getIdent(zero.name); - if (std.mem.eql(u8, name_text, "True") or std.mem.eql(u8, name_text, "False")) { + if (zero.name == self.env.true_tag_ident or zero.name == self.env.false_tag_ident) { rt_var = try self.getCanonicalBoolRuntimeVar(); resolved = self.runtime_types.resolveVar(rt_var); } @@ -1447,8 +1446,7 @@ pub const Interpreter = struct { var resolved = self.resolveBaseVar(rt_var); // If the type is still flex and this is a True/False tag, use Bool if (resolved.desc.content == .flex) { - const name_text = self.env.getIdent(tag.name); - if (std.mem.eql(u8, name_text, "True") or std.mem.eql(u8, name_text, "False")) { + if (tag.name == self.env.true_tag_ident or tag.name == self.env.false_tag_ident) { rt_var = try self.getCanonicalBoolRuntimeVar(); resolved = self.resolveBaseVar(rt_var); } @@ -1802,13 +1800,19 @@ pub const Interpreter = struct { return error.Crash; }, .e_dbg => |dbg_expr| { + // Evaluate and print the inner expression const inner_ct_var = can.ModuleEnv.varFrom(dbg_expr.expr); const inner_rt_var = try self.translateTypeVar(self.env, inner_ct_var); const value = try self.evalExprMinimal(dbg_expr.expr, roc_ops, inner_rt_var); + defer value.decref(&self.runtime_layout_store, roc_ops); const rendered = try self.renderValueRocWithType(value, inner_rt_var); defer self.allocator.free(rendered); roc_ops.dbg(rendered); - return value; + // dbg returns {} (empty record) - use same pattern as e_expect + const ct_var = can.ModuleEnv.varFrom(expr_idx); + const rt_var = try self.translateTypeVar(self.env, ct_var); + const layout_val = try self.getRuntimeLayout(rt_var); + return try self.pushRaw(layout_val, 0); }, // no tag handling in minimal evaluator .e_lambda => |lam| { diff --git a/src/parse/Parser.zig b/src/parse/Parser.zig index 989dbeb1b6..712efebc60 100644 --- a/src/parse/Parser.zig +++ b/src/parse/Parser.zig @@ -1111,7 +1111,7 @@ fn parseStmtByType(self: *Parser, statementType: StatementType) Error!AST.Statem } }); return statement_idx; }, - .KwDbg, .KwDebug => { + .KwDbg => { const start = self.pos; self.advance(); const expr = try self.parseExpr(); @@ -2145,7 +2145,7 @@ pub fn parseExprWithBp(self: *Parser, min_bp: u8) Error!AST.Expr.Idx { .branches = branches, } }); }, - .KwDbg, .KwDebug => { + .KwDbg => { self.advance(); const e = try self.parseExpr(); expr = try self.store.addExpr(.{ .dbg = .{ diff --git a/src/parse/tokenize.zig b/src/parse/tokenize.zig index c9b8fdd25f..779a269b6c 100644 --- a/src/parse/tokenize.zig +++ b/src/parse/tokenize.zig @@ -135,7 +135,6 @@ pub const Token = struct { KwAs, KwCrash, KwDbg, - KwDebug, KwElse, KwExpect, KwExposes, @@ -275,7 +274,6 @@ pub const Token = struct { .KwAs, .KwCrash, .KwDbg, - .KwDebug, .KwElse, .KwExpect, .KwExposes, @@ -369,7 +367,6 @@ pub const Token = struct { .{ "as", .KwAs }, .{ "crash", .KwCrash }, .{ "dbg", .KwDbg }, - .{ "debug", .KwDebug }, .{ "else", .KwElse }, .{ "expect", .KwExpect }, .{ "exposes", .KwExposes }, @@ -2210,9 +2207,6 @@ fn rebuildBufferForTesting(buf: []const u8, tokens: *TokenizedBuffer, alloc: std .KwDbg => { try buf2.appendSlice("dbg"); }, - .KwDebug => { - try buf2.appendSlice("debug"); - }, .KwElse => { try buf2.appendSlice("else"); }, diff --git a/src/playground_wasm/main.zig b/src/playground_wasm/main.zig index eddb8b4d69..23db5c35a9 100644 --- a/src/playground_wasm/main.zig +++ b/src/playground_wasm/main.zig @@ -1040,6 +1040,9 @@ fn compileSource(source: []const u8) !CompilerStageData { .unbox_method_ident = common.findIdent("unbox") orelse unreachable, .ok_ident = common.findIdent("Ok") orelse unreachable, .err_ident = common.findIdent("Err") orelse unreachable, + .numeral_relative_ident = common.findIdent("Num.Numeral") orelse unreachable, + .true_tag_ident = common.findIdent("True") orelse unreachable, + .false_tag_ident = common.findIdent("False") orelse unreachable, .deferred_numeric_literals = try ModuleEnv.DeferredNumericLiteral.SafeList.initCapacity(gpa, 0), .import_mapping = types.import_mapping.ImportMapping.init(gpa), }; diff --git a/src/repl/repl_test.zig b/src/repl/repl_test.zig index 58c61fa767..49a46ca831 100644 --- a/src/repl/repl_test.zig +++ b/src/repl/repl_test.zig @@ -140,6 +140,9 @@ fn loadCompiledModule(gpa: std.mem.Allocator, bin_data: []const u8, module_name: .unbox_method_ident = common.findIdent("unbox") orelse unreachable, .ok_ident = common.findIdent("Ok") orelse unreachable, .err_ident = common.findIdent("Err") orelse unreachable, + .numeral_relative_ident = common.findIdent("Num.Numeral") orelse unreachable, + .true_tag_ident = common.findIdent("True") orelse unreachable, + .false_tag_ident = common.findIdent("False") orelse unreachable, .deferred_numeric_literals = try ModuleEnv.DeferredNumericLiteral.SafeList.initCapacity(gpa, 0), .import_mapping = types.import_mapping.ImportMapping.init(gpa), }; diff --git a/src/reporting/common_misspellings.zig b/src/reporting/common_misspellings.zig index 5c775bd4a3..f9cc526d7a 100644 --- a/src/reporting/common_misspellings.zig +++ b/src/reporting/common_misspellings.zig @@ -22,6 +22,7 @@ pub const CommonMisspellings = struct { .{ "case", "`case` is not a keyword in Roc. Use `match` for pattern matching." }, .{ "switch", "`switch` is not a keyword in Roc. Use `match` for pattern matching." }, .{ "when", "`when` is not a keyword in Roc. Use `match` for pattern matching." }, + .{ "debug", "`debug` is not a keyword in Roc. Use `dbg` for debug printing." }, .{ "then", "`then` is not a keyword in Roc. You can put the first branch of an `if` immediately after the condition, e.g. `if (condition) then_branch else else_branch`" }, .{ "elif", "Roc uses `else if` for chaining conditions, not `elif`." }, .{ "elseif", "Roc uses `else if` (two words) for chaining conditions." }, @@ -107,7 +108,7 @@ test "identifier misspellings lookup" { const tip = CommonMisspellings.getIdentifierTip("case"); try std.testing.expect(tip != null); try std.testing.expectEqualStrings( - "`case` is not a keyword in Roc. Use `when` for pattern matching.", + "`case` is not a keyword in Roc. Use `match` for pattern matching.", tip.?, ); } diff --git a/test/fx/dbg_missing_return.roc b/test/fx/dbg_missing_return.roc new file mode 100644 index 0000000000..c6da215ca5 --- /dev/null +++ b/test/fx/dbg_missing_return.roc @@ -0,0 +1,7 @@ +app [main!] { pf: platform "./platform/main.roc" } + +import pf.Stdout + +main! = || { + dbg "this should work now" +} diff --git a/test/playground-integration/main.zig b/test/playground-integration/main.zig index 5f3cd7aa31..243459e25f 100644 --- a/test/playground-integration/main.zig +++ b/test/playground-integration/main.zig @@ -433,7 +433,8 @@ fn setupWasm(gpa: std.mem.Allocator, arena: std.mem.Allocator, wasm_path: []cons // Create and instantiate the module instance using the gpa allocator for the VM var module_instance = try bytebox.createModuleInstance(.Stack, module_def, gpa); errdefer module_instance.destroy(); - try module_instance.instantiate(.{}); + // Use a larger stack size (256 KB instead of default 128 KB) to accommodate complex interpreter code + try module_instance.instantiate(.{ .stack_size = 1024 * 256 }); logDebug("[INFO] WASM module instantiated successfully.\n", .{}); diff --git a/test/snapshots/fuzz_crash/fuzz_crash_023.md b/test/snapshots/fuzz_crash/fuzz_crash_023.md index 25c7656bbe..a71ac2cfdd 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_023.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_023.md @@ -281,6 +281,7 @@ UNUSED VALUE - fuzz_crash_023.md:1:1:1:1 TYPE MISMATCH - fuzz_crash_023.md:155:2:157:3 UNUSED VALUE - fuzz_crash_023.md:155:2:157:3 UNUSED VALUE - fuzz_crash_023.md:178:42:178:45 +TYPE MISMATCH - fuzz_crash_023.md:144:9:196:2 # PROBLEMS **PARSE ERROR** A parsing error occurred: `expected_expr_record_field_name` @@ -1046,6 +1047,71 @@ This expression produces a value, but it's not being used: It has the type: _[Blue]_others_ +**TYPE MISMATCH** +This expression is used in an unexpected way: +**fuzz_crash_023.md:144:9:196:2:** +```roc +main! = |_| { # Yeah I can leave a comment here + world = "World" + var number = 123 + expect blah == 1 + tag = Blue + return # Comment after return keyword + tag # Comment after return statement + + # Just a random comment! + + ... + match_time( + ..., # Single args with comment + ) + some_func( + dbg # After debug + 42, # After debug expr + ) + crash # Comment after crash keyword + "Unreachable!" # Comment after crash statement + tag_with_payload = Ok(number) + interpolated = "Hello, ${world}" + list = [ + add_one( + dbg # After dbg in list + number, # after dbg expr as arg + ), # Comment one + 456, # Comment two + 789, # Comment three + ] + for n in list { + Stdout.line!("Adding ${n} to ${number}") + number = number + n + } + record = { foo: 123, bar: "Hello", ;az: tag, qux: Ok(world), punned } + tuple = (123, "World", tag, Ok(world), (nested, tuple), [1, 2, 3]) + multiline_tuple = ( + 123, + "World", + tag1, + Ok(world), # This one has a comment + (nested, tuple), + [1, 2, 3], + ) + bin_op_result = Err(foo) ?? 12 > 5 * 5 or 13 + 2 < 5 and 10 - 1 >= 16 or 12 <= 3 / 5 + static_dispatch_style = some_fn(arg1)?.static_dispatch_method()?.next_static_dispatch_method()?.record_field? + Stdout.line!(interpolated)? + Stdout.line!( + "How about ${ # Comment after string interpolation open + Num.toStr(number) # Comment after string interpolation expr + } as a string?", + ) +} # Comment after top-level decl +``` + +It has the type: + _List(Error) => Error_ + +But the type annotation says it should have the type: + _List(Error) -> Error_ + # TOKENS ~~~zig KwApp,OpenSquare,LowerIdent,CloseSquare,OpenCurly,LowerIdent,OpColon,KwPlatform,StringStart,StringPart,StringEnd,CloseCurly, @@ -2577,7 +2643,7 @@ expect { (patt (type "Error -> U64")) (patt (type "[Red][Blue, Green][ProvidedByCompiler], _arg -> Error")) (patt (type "Error")) - (patt (type "List(Error) -> Error")) + (patt (type "Error")) (patt (type "{}")) (patt (type "Error"))) (type_decls @@ -2624,7 +2690,7 @@ expect { (expr (type "Error -> U64")) (expr (type "[Red][Blue, Green][ProvidedByCompiler], _arg -> Error")) (expr (type "Error")) - (expr (type "List(Error) -> Error")) + (expr (type "Error")) (expr (type "{}")) (expr (type "Error")))) ~~~ diff --git a/test/snapshots/fuzz_crash/fuzz_crash_027.md b/test/snapshots/fuzz_crash/fuzz_crash_027.md index 3b70f4d233..6933cb1017 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_027.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_027.md @@ -231,6 +231,7 @@ UNUSED VALUE - fuzz_crash_027.md:1:1:1:1 TYPE MISMATCH - fuzz_crash_027.md:111:2:113:3 UNUSED VALUE - fuzz_crash_027.md:111:2:113:3 TYPE MISMATCH - fuzz_crash_027.md:143:2:147:3 +TYPE MISMATCH - fuzz_crash_027.md:100:9:148:2 # PROBLEMS **LEADING ZERO** Numbers cannot have leading zeros. @@ -972,6 +973,67 @@ It has the type: But the type annotation says it should have the type: _Try(d)_ +**TYPE MISMATCH** +This expression is used in an unexpected way: +**fuzz_crash_027.md:100:9:148:2:** +```roc +main! = |_| { # Yeah Ie + world = "World" + var number = 123 + expect blah == 1 + tag = Blue + return # Comd + tag + + # Jusnt! + + ... + match_time( + ..., # + ) + some_func( + dbg # bug + 42, # Aft expr + ) + crash "Unreachtement + tag_with = Ok(number) + ited = "Hello, ${world}" + list = [ + add_one( + dbg # Afin list +e[, # afarg + ), 456, # ee + ] + for n in list { + line!("Adding ${n} to ${number}") + number = number + n + } + record = { foo: 123, bar: "Hello", baz: tag, qux: Ok(world), punned } + tuple = (123, "World", tag, Ok(world), (nested, tuple), [1, 2, 3]) + m_tuple = ( + 123, + "World", + tag1, + Ok(world), # Thisnt + (nested, tuple), + [1, 2, 3], + ) + bsult = Err(foo) ?? 12 > 5 * 5 or 13 + 2 < 5 and 10 - 1 >= 16 or 12 <= 3 / 5 + stale = some_fn(arg1)?.statod()?.ned()?.recd? + Stdoline!( + "How about ${ # + Num.toStr(number) # on expr + } as a", + ) +} # Commenl decl +``` + +It has the type: + _List(Error) => Error_ + +But the type annotation says it should have the type: + _List(Error) -> Error_ + # TOKENS ~~~zig KwApp,OpenSquare,LowerIdent,CloseSquare,OpenCurly,LowerIdent,OpColon,KwPlatform,StringStart,StringPart,StringEnd,CloseCurly, @@ -2255,7 +2317,7 @@ expect { (patt (type "Bool -> d where [d.from_numeral : Numeral -> Try(d, [InvalidNumeral(Str)])]")) (patt (type "Error -> U64")) (patt (type "[Red, Blue][ProvidedByCompiler], _arg -> Error")) - (patt (type "List(Error) -> Error")) + (patt (type "Error")) (patt (type "{}")) (patt (type "Error"))) (type_decls @@ -2292,7 +2354,7 @@ expect { (expr (type "Bool -> d where [d.from_numeral : Numeral -> Try(d, [InvalidNumeral(Str)])]")) (expr (type "Error -> U64")) (expr (type "[Red, Blue][ProvidedByCompiler], _arg -> Error")) - (expr (type "List(Error) -> Error")) + (expr (type "Error")) (expr (type "{}")) (expr (type "Error")))) ~~~ diff --git a/test/snapshots/fuzz_crash/fuzz_crash_028.md b/test/snapshots/fuzz_crash/fuzz_crash_028.md index c69dd3a42f..5591020f4d 100644 Binary files a/test/snapshots/fuzz_crash/fuzz_crash_028.md and b/test/snapshots/fuzz_crash/fuzz_crash_028.md differ diff --git a/test/snapshots/repl/try_is_eq.md b/test/snapshots/repl/try_is_eq.md index e7d4139b8f..2819a5a068 100644 --- a/test/snapshots/repl/try_is_eq.md +++ b/test/snapshots/repl/try_is_eq.md @@ -13,16 +13,16 @@ type=repl ยป Try.Ok(1) != Try.Ok(1) ~~~ # OUTPUT -Evaluation error: error.BugUnboxedRigidVar +Crash: e_closure: failed to resolve capture value --- -Evaluation error: error.BugUnboxedRigidVar +Crash: e_closure: failed to resolve capture value --- -Evaluation error: error.BugUnboxedRigidVar +Crash: e_closure: failed to resolve capture value --- -Evaluation error: error.BugUnboxedRigidVar +Crash: e_closure: failed to resolve capture value --- -Evaluation error: error.BugUnboxedRigidVar +Crash: e_closure: failed to resolve capture value --- -Evaluation error: error.BugUnboxedRigidVar +Crash: e_closure: failed to resolve capture value # PROBLEMS NIL diff --git a/test/snapshots/statement/dbg_as_arg.md b/test/snapshots/statement/dbg_as_arg.md new file mode 100644 index 0000000000..f478ce37bc --- /dev/null +++ b/test/snapshots/statement/dbg_as_arg.md @@ -0,0 +1,84 @@ +# META +~~~ini +description=Debug as function argument +type=snippet +~~~ +# SOURCE +~~~roc +foo = |f| f(dbg 42) +bar = |f| f(dbg(42)) +~~~ +# EXPECTED +NIL +# PROBLEMS +NIL +# TOKENS +~~~zig +LowerIdent,OpAssign,OpBar,LowerIdent,OpBar,LowerIdent,NoSpaceOpenRound,KwDbg,Int,CloseRound, +LowerIdent,OpAssign,OpBar,LowerIdent,OpBar,LowerIdent,NoSpaceOpenRound,KwDbg,NoSpaceOpenRound,Int,CloseRound,CloseRound, +EndOfFile, +~~~ +# PARSE +~~~clojure +(file + (type-module) + (statements + (s-decl + (p-ident (raw "foo")) + (e-lambda + (args + (p-ident (raw "f"))) + (e-apply + (e-ident (raw "f")) + (e-dbg + (e-int (raw "42")))))) + (s-decl + (p-ident (raw "bar")) + (e-lambda + (args + (p-ident (raw "f"))) + (e-apply + (e-ident (raw "f")) + (e-dbg + (e-tuple + (e-int (raw "42"))))))))) +~~~ +# FORMATTED +~~~roc +foo = |f| f(dbg 42) +bar = |f| f(dbg (42)) +~~~ +# CANONICALIZE +~~~clojure +(can-ir + (d-let + (p-assign (ident "foo")) + (e-lambda + (args + (p-assign (ident "f"))) + (e-call + (e-lookup-local + (p-assign (ident "f"))) + (e-dbg + (e-num (value "42")))))) + (d-let + (p-assign (ident "bar")) + (e-lambda + (args + (p-assign (ident "f"))) + (e-call + (e-lookup-local + (p-assign (ident "f"))) + (e-dbg + (e-num (value "42"))))))) +~~~ +# TYPES +~~~clojure +(inferred-types + (defs + (patt (type "({} -> a) => a")) + (patt (type "({} -> a) => a"))) + (expressions + (expr (type "({} -> a) => a")) + (expr (type "({} -> a) => a")))) +~~~ diff --git a/test/snapshots/statement/dbg_last_in_block.md b/test/snapshots/statement/dbg_last_in_block.md new file mode 100644 index 0000000000..f53f4e46a5 --- /dev/null +++ b/test/snapshots/statement/dbg_last_in_block.md @@ -0,0 +1,63 @@ +# META +~~~ini +description=Debug as last expression in block should return {} +type=snippet +~~~ +# SOURCE +~~~roc +main = || { + dbg "hello" +} +~~~ +# EXPECTED +NIL +# PROBLEMS +NIL +# TOKENS +~~~zig +LowerIdent,OpAssign,OpBar,OpBar,OpenCurly, +KwDbg,StringStart,StringPart,StringEnd, +CloseCurly, +EndOfFile, +~~~ +# PARSE +~~~clojure +(file + (type-module) + (statements + (s-decl + (p-ident (raw "main")) + (e-lambda + (args) + (e-block + (statements + (s-dbg + (e-string + (e-string-part (raw "hello")))))))))) +~~~ +# FORMATTED +~~~roc +main = || { + dbg "hello" +} +~~~ +# CANONICALIZE +~~~clojure +(can-ir + (d-let + (p-assign (ident "main")) + (e-lambda + (args) + (e-block + (e-dbg + (e-string + (e-literal (string "hello")))))))) +~~~ +# TYPES +~~~clojure +(inferred-types + (defs + (patt (type "({}) => {}"))) + (expressions + (expr (type "({}) => {}")))) +~~~ diff --git a/test/snapshots/statement/dbg_simple_test.md b/test/snapshots/statement/dbg_simple_test.md index 0b78b3d462..d295eff87d 100644 --- a/test/snapshots/statement/dbg_simple_test.md +++ b/test/snapshots/statement/dbg_simple_test.md @@ -62,7 +62,7 @@ test = { ~~~clojure (inferred-types (defs - (patt (type "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]"))) + (patt (type "{}"))) (expressions - (expr (type "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")))) + (expr (type "{}")))) ~~~ diff --git a/test/snapshots/statement/dbg_stmt_block_example.md b/test/snapshots/statement/dbg_stmt_block_example.md index bac8ce59a2..39cfa9420a 100644 --- a/test/snapshots/statement/dbg_stmt_block_example.md +++ b/test/snapshots/statement/dbg_stmt_block_example.md @@ -79,7 +79,7 @@ foo = |num| { ~~~clojure (inferred-types (defs - (patt (type "a -> a where [a.to_str : a -> b]"))) + (patt (type "a => {} where [a.to_str : a -> _ret]"))) (expressions - (expr (type "a -> a where [a.to_str : a -> b]")))) + (expr (type "a => {} where [a.to_str : a -> _ret]")))) ~~~ diff --git a/test/snapshots/syntax_grab_bag.md b/test/snapshots/syntax_grab_bag.md index d48f85ade4..f7c806aa27 100644 --- a/test/snapshots/syntax_grab_bag.md +++ b/test/snapshots/syntax_grab_bag.md @@ -271,6 +271,7 @@ INCOMPATIBLE MATCH PATTERNS - syntax_grab_bag.md:84:2:84:2 UNUSED VALUE - syntax_grab_bag.md:1:1:1:1 TYPE MISMATCH - syntax_grab_bag.md:155:2:157:3 UNUSED VALUE - syntax_grab_bag.md:155:2:157:3 +TYPE MISMATCH - syntax_grab_bag.md:144:9:196:2 # PROBLEMS **UNDECLARED TYPE** The type _Bar_ is not declared in this scope. @@ -926,6 +927,71 @@ This expression produces a value, but it's not being used: It has the type: _d_ +**TYPE MISMATCH** +This expression is used in an unexpected way: +**syntax_grab_bag.md:144:9:196:2:** +```roc +main! = |_| { # Yeah I can leave a comment here + world = "World" + var number = 123 + expect blah == 1 + tag = Blue + return # Comment after return keyword + tag # Comment after return statement + + # Just a random comment! + + ... + match_time( + ..., # Single args with comment + ) + some_func( + dbg # After debug + 42, # After debug expr + ) + crash # Comment after crash keyword + "Unreachable!" # Comment after crash statement + tag_with_payload = Ok(number) + interpolated = "Hello, ${world}" + list = [ + add_one( + dbg # After dbg in list + number, # after dbg expr as arg + ), # Comment one + 456, # Comment two + 789, # Comment three + ] + for n in list { + Stdout.line!("Adding ${n} to ${number}") + number = number + n + } + record = { foo: 123, bar: "Hello", baz: tag, qux: Ok(world), punned } + tuple = (123, "World", tag, Ok(world), (nested, tuple), [1, 2, 3]) + multiline_tuple = ( + 123, + "World", + tag1, + Ok(world), # This one has a comment + (nested, tuple), + [1, 2, 3], + ) + bin_op_result = Err(foo) ?? 12 > 5 * 5 or 13 + 2 < 5 and 10 - 1 >= 16 or 12 <= 3 / 5 + static_dispatch_style = some_fn(arg1)?.static_dispatch_method()?.next_static_dispatch_method()?.record_field? + Stdout.line!(interpolated)? + Stdout.line!( + "How about ${ # Comment after string interpolation open + Num.toStr(number) # Comment after string interpolation expr + } as a string?", + ) +} # Comment after top-level decl +``` + +It has the type: + _List(Error) => Error_ + +But the type annotation says it should have the type: + _List(Error) -> Error_ + # TOKENS ~~~zig KwApp,OpenSquare,LowerIdent,CloseSquare,OpenCurly,LowerIdent,OpColon,KwPlatform,StringStart,StringPart,StringEnd,CloseCurly, @@ -2462,7 +2528,7 @@ expect { (patt (type "Bool -> d where [d.from_numeral : Numeral -> Try(d, [InvalidNumeral(Str)])]")) (patt (type "Error -> U64")) (patt (type "[Red][Blue, Green][ProvidedByCompiler], _arg -> Error")) - (patt (type "List(Error) -> Error")) + (patt (type "Error")) (patt (type "{}")) (patt (type "Error"))) (type_decls @@ -2508,7 +2574,7 @@ expect { (expr (type "Bool -> d where [d.from_numeral : Numeral -> Try(d, [InvalidNumeral(Str)])]")) (expr (type "Error -> U64")) (expr (type "[Red][Blue, Green][ProvidedByCompiler], _arg -> Error")) - (expr (type "List(Error) -> Error")) + (expr (type "Error")) (expr (type "{}")) (expr (type "Error")))) ~~~