diff --git a/src/check/Check.zig b/src/check/Check.zig index c4a09db33b..1d82cb4285 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -4056,6 +4056,9 @@ fn handleRecursiveConstraint( /// process that, we then have to check `Test.to_str`. fn checkDeferredStaticDispatchConstraints(self: *Self, env: *Env) std.mem.Allocator.Error!void { var deferred_constraint_len = env.deferred_static_dispatch_constraints.items.items.len; + if (deferred_constraint_len > 0) { + std.debug.print("DEBUG: checkDeferredStaticDispatchConstraints called with {} constraints\n", .{deferred_constraint_len}); + } var deferred_constraint_index: usize = 0; while (deferred_constraint_index < deferred_constraint_len) : ({ deferred_constraint_index += 1; @@ -4196,6 +4199,12 @@ fn checkDeferredStaticDispatchConstraints(self: *Self, env: *Env) std.mem.Alloca const region = self.getRegionAt(deferred_constraint.var_); const type_name_bytes = self.cir.getIdent(nominal_type.ident.ident_idx); + std.debug.print("DEBUG Check: type_name_bytes='{s}', original_module_ident={}, original_env.module_name='{s}'\n", .{ + type_name_bytes, + original_module_ident, + original_env.module_name, + }); + // Iterate over the constraints const constraints = self.types.sliceStaticDispatchConstraints(deferred_constraint.constraints); for (constraints) |constraint| { @@ -4211,27 +4220,10 @@ fn checkDeferredStaticDispatchConstraints(self: *Self, env: *Env) std.mem.Alloca // Calculate the name of the static dispatch function // - // For builtin types like "Builtin.Try", the type ident includes the module prefix, - // but methods are registered as "Try.ok_or" not "Builtin.Builtin.Try.ok_or". - // We need to strip the "Builtin." prefix for method lookup. - // // TODO: This works for top-level types, but not for deeply // nested types like: MyModule.A.B.C.my_func self.static_dispatch_method_name_buf.clearRetainingCapacity(); - - const is_builtin_nested_type = original_module_ident == self.cir.builtin_module_ident and - std.mem.startsWith(u8, type_name_bytes, "Builtin."); - - if (is_builtin_nested_type) { - // For nested builtin types like "Builtin.Try", strip the "Builtin." prefix - // Method is registered as "Try.ok_or" - const short_name = type_name_bytes[8..]; // Skip "Builtin." - try self.static_dispatch_method_name_buf.print( - self.gpa, - "{s}.{s}", - .{ short_name, constraint_fn_name_bytes }, - ); - } else if (std.mem.eql(u8, type_name_bytes, original_env.module_name)) { + if (std.mem.eql(u8, type_name_bytes, original_env.module_name)) { try self.static_dispatch_method_name_buf.print( self.gpa, "{s}.{s}", diff --git a/src/compile/compile_package.zig b/src/compile/compile_package.zig index 1a723e44e3..6617cc7541 100644 --- a/src/compile/compile_package.zig +++ b/src/compile/compile_package.zig @@ -770,12 +770,17 @@ pub const PackageEnv = struct { } } - fn doTypeCheck(self: *PackageEnv, module_id: ModuleId) !void { - var st = &self.modules.items[module_id]; - var env = &st.env.?; - + /// Standalone type checking function that can be called from other tools (e.g., snapshot tool) + /// This ensures all tools use the exact same type checking logic as production builds + pub fn typeCheckModule( + gpa: Allocator, + env: *ModuleEnv, + builtin_module_env: *const ModuleEnv, + imported_envs: []const *ModuleEnv, + ) !Check { + std.debug.print("DEBUG: typeCheckModule called\n", .{}); // Load builtin indices from the binary data generated at build time - const builtin_indices = try builtin_loading.deserializeBuiltinIndices(self.gpa, compiled_builtins.builtin_indices_bin); + const builtin_indices = try builtin_loading.deserializeBuiltinIndices(gpa, compiled_builtins.builtin_indices_bin); const module_common_idents: Check.CommonIdents = .{ .module_name = try env.insertIdent(base.Ident.for_text("test")), @@ -783,21 +788,48 @@ pub const PackageEnv = struct { .box = try env.insertIdent(base.Ident.for_text("Box")), .bool_stmt = builtin_indices.bool_type, .try_stmt = builtin_indices.try_type, - .builtin_module = self.builtin_modules.builtin_module.env, + .builtin_module = builtin_module_env, }; // Create module_envs map for auto-importing builtin types - var module_envs_map = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(self.gpa); - defer module_envs_map.deinit(); + var module_envs_map = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(gpa); + errdefer module_envs_map.deinit(); // Populate module_envs with Bool, Try, Dict, Set using shared function try Can.populateModuleEnvs( &module_envs_map, env, - self.builtin_modules.builtin_module.env, + builtin_module_env, builtin_indices, ); + var checker = try Check.init( + gpa, + &env.types, + env, + imported_envs, + &module_envs_map, + &env.store.regions, + module_common_idents, + ); + errdefer checker.deinit(); + + try checker.checkFile(); + + // After type checking, evaluate top-level declarations at compile time + const builtin_types_for_eval = BuiltinTypes.init(builtin_indices, builtin_module_env, builtin_module_env, builtin_module_env); + var comptime_evaluator = try eval.ComptimeEvaluator.init(gpa, env, imported_envs, &checker.problems, builtin_types_for_eval); + _ = try comptime_evaluator.evalAll(); + + module_envs_map.deinit(); + + return checker; + } + + fn doTypeCheck(self: *PackageEnv, module_id: ModuleId) !void { + var st = &self.modules.items[module_id]; + var env = &st.env.?; + // Build other_modules array according to env.imports order const import_count = env.imports.imports.items.items.len; var imported_envs = try std.ArrayList(*ModuleEnv).initCapacity(self.gpa, import_count); @@ -826,34 +858,15 @@ pub const PackageEnv = struct { } } - var checker = try Check.init( - self.gpa, - &env.types, - env, - imported_envs.items, - &module_envs_map, - &env.store.regions, - module_common_idents, - ); - defer checker.deinit(); - // Note: checkDefs runs type checking for module + std.debug.print("DEBUG doTypeCheck: calling typeCheckModule\n", .{}); const check_start = if (@import("builtin").target.cpu.arch != .wasm32) std.time.nanoTimestamp() else 0; - try checker.checkFile(); + var checker = try typeCheckModule(self.gpa, env, self.builtin_modules.builtin_module.env, imported_envs.items); + defer checker.deinit(); const check_end = if (@import("builtin").target.cpu.arch != .wasm32) std.time.nanoTimestamp() else 0; if (@import("builtin").target.cpu.arch != .wasm32) { self.total_type_checking_ns += @intCast(check_end - check_start); } - // After type checking, evaluate top-level declarations at compile time - // Load builtin module required by the interpreter (reuse builtin_indices from above) - const builtin_source = compiled_builtins.builtin_source; - var builtin_module = try builtin_loading.loadCompiledModule(self.gpa, compiled_builtins.builtin_bin, "Builtin", builtin_source); - 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 = try eval.ComptimeEvaluator.init(self.gpa, env, imported_envs.items, &checker.problems, builtin_types_for_eval); - _ = try comptime_evaluator.evalAll(); - // Build reports from problems const check_diag_start = if (@import("builtin").target.cpu.arch != .wasm32) std.time.nanoTimestamp() else 0; var rb = ReportBuilder.init(self.gpa, env, env, &checker.snapshots, st.path, imported_envs.items, &checker.import_mapping); @@ -867,8 +880,7 @@ pub const PackageEnv = struct { self.total_check_diagnostics_ns += @intCast(check_diag_end - check_diag_start); } - // Clean up comptime evaluator AFTER building reports (crash messages must stay alive until reports are built) - comptime_evaluator.deinit(); + // Comptime evaluator is managed inside typeCheckModule, no need to deinit here // Now we can safely deinit the 'imported_envs' ArrayList imported_envs.deinit(self.gpa); diff --git a/src/snapshot_tool/main.zig b/src/snapshot_tool/main.zig index 2af73c72f8..346b92c42d 100644 --- a/src/snapshot_tool/main.zig +++ b/src/snapshot_tool/main.zig @@ -648,87 +648,6 @@ fn extractSectionInfo(content: []const u8, section_name: []const u8) ?struct { s return .{ .start = start_idx, .end = next_section_idx }; } -/// Wrapper for a loaded compiled builtin module that tracks the buffer -const LoadedModule = struct { - env: *ModuleEnv, - buffer: []align(collections.CompactWriter.SERIALIZATION_ALIGNMENT.toByteUnits()) u8, - gpa: std.mem.Allocator, - - fn deinit(self: *LoadedModule) void { - // Only free the hashmap that was allocated during deserialization - // Most other data (like the SafeList contents) points into the buffer - self.env.imports.map.deinit(self.gpa); - - // Free the buffer (the env points into this buffer for most data) - self.gpa.free(self.buffer); - // Free the env struct itself - self.gpa.destroy(self.env); - } -}; - -/// Load a compiled ModuleEnv from embedded binary data -fn loadCompiledModule(gpa: std.mem.Allocator, bin_data: []const u8, module_name: []const u8, source: []const u8) !LoadedModule { - // Copy the embedded data to properly aligned memory - // CompactWriter requires specific alignment for serialization - const CompactWriter = collections.CompactWriter; - const buffer = try gpa.alignedAlloc(u8, CompactWriter.SERIALIZATION_ALIGNMENT, bin_data.len); - @memcpy(buffer, bin_data); - - // Cast to the serialized structure - const serialized_ptr = @as( - *ModuleEnv.Serialized, - @ptrCast(@alignCast(buffer.ptr)), - ); - - const env = try gpa.create(ModuleEnv); - errdefer gpa.destroy(env); - - // Deserialize - const base_ptr = @intFromPtr(buffer.ptr); - - // Deserialize common env first so we can look up identifiers - const common = serialized_ptr.common.deserialize(@as(i64, @intCast(base_ptr)), source).*; - - env.* = ModuleEnv{ - .gpa = gpa, - .common = common, - .types = serialized_ptr.types.deserialize(@as(i64, @intCast(base_ptr)), gpa).*, - .module_kind = serialized_ptr.module_kind, - .all_defs = serialized_ptr.all_defs, - .all_statements = serialized_ptr.all_statements, - .exports = serialized_ptr.exports, - .builtin_statements = serialized_ptr.builtin_statements, - .external_decls = serialized_ptr.external_decls.deserialize(@as(i64, @intCast(base_ptr))).*, - .imports = (try serialized_ptr.imports.deserialize(@as(i64, @intCast(base_ptr)), gpa)).*, - .module_name = module_name, - .module_name_idx = undefined, // Not used for deserialized modules (only needed during fresh canonicalization) - .diagnostics = serialized_ptr.diagnostics, - .store = serialized_ptr.store.deserialize(@as(i64, @intCast(base_ptr)), gpa).*, - .evaluation_order = null, - .from_int_digits_ident = common.findIdent(base.Ident.FROM_INT_DIGITS_METHOD_NAME) orelse unreachable, - .from_dec_digits_ident = common.findIdent(base.Ident.FROM_DEC_DIGITS_METHOD_NAME) orelse unreachable, - .try_ident = common.findIdent("Try") orelse unreachable, - .out_of_range_ident = common.findIdent("OutOfRange") orelse unreachable, - .builtin_module_ident = common.findIdent("Builtin") orelse unreachable, - .plus_ident = common.findIdent(base.Ident.PLUS_METHOD_NAME) orelse unreachable, - }; - - return LoadedModule{ - .env = env, - .buffer = buffer, - .gpa = gpa, - }; -} - -/// Deserialize BuiltinIndices from the binary data generated at build time -fn deserializeBuiltinIndices(gpa: Allocator, bin_data: []const u8) !CIR.BuiltinIndices { - const aligned_buffer = try gpa.alignedAlloc(u8, @enumFromInt(@alignOf(CIR.BuiltinIndices)), bin_data.len); - defer gpa.free(aligned_buffer); - @memcpy(aligned_buffer, bin_data); - const indices_ptr = @as(*const CIR.BuiltinIndices, @ptrCast(aligned_buffer.ptr)); - return indices_ptr.*; -} - var debug_allocator: std.heap.DebugAllocator(.{}) = .{ .backing_allocator = std.heap.c_allocator, }; @@ -868,12 +787,12 @@ pub fn main() !void { } } - // Load compiled Builtin module (contains nested Bool, Try, Str, Dict, Set) - const builtin_source = compiled_builtins.builtin_source; - var builtin_loaded = try loadCompiledModule(gpa, compiled_builtins.builtin_bin, "Builtin", builtin_source); - defer builtin_loaded.deinit(); + // Load builtin modules using the same code path as roc check + const builtin_modules_ptr = try gpa.create(eval_mod.BuiltinModules); + defer gpa.destroy(builtin_modules_ptr); - const builtin_indices = try deserializeBuiltinIndices(gpa, compiled_builtins.builtin_indices_bin); + builtin_modules_ptr.* = try eval_mod.BuiltinModules.init(gpa); + defer builtin_modules_ptr.deinit(); const config = Config{ .maybe_fuzz_corpus_path = maybe_fuzz_corpus_path, @@ -882,8 +801,8 @@ pub fn main() !void { .output_section_command = output_section_command, .trace_eval = trace_eval, .linecol_mode = linecol_mode, - .builtin_module = builtin_loaded.env, - .builtin_indices = builtin_indices, + .builtin_module = builtin_modules_ptr.builtin_module.env, + .builtin_indices = builtin_modules_ptr.builtin_indices, }; if (config.maybe_fuzz_corpus_path != null) { @@ -921,12 +840,12 @@ pub fn main() !void { } fn checkSnapshotExpectations(gpa: Allocator) !bool { - // Load compiled Builtin module (contains nested Bool, Try, Str, Dict, Set) - const builtin_source = compiled_builtins.builtin_source; - var builtin_loaded = try loadCompiledModule(gpa, compiled_builtins.builtin_bin, "Builtin", builtin_source); - defer builtin_loaded.deinit(); + // Load builtin modules using the same code path as roc check + const builtin_modules_ptr = try gpa.create(eval_mod.BuiltinModules); + defer gpa.destroy(builtin_modules_ptr); - const builtin_indices = try deserializeBuiltinIndices(gpa, compiled_builtins.builtin_indices_bin); + builtin_modules_ptr.* = try eval_mod.BuiltinModules.init(gpa); + defer builtin_modules_ptr.deinit(); const config = Config{ .maybe_fuzz_corpus_path = null, @@ -934,8 +853,8 @@ fn checkSnapshotExpectations(gpa: Allocator) !bool { .expected_section_command = .check, .output_section_command = .check, .disable_updates = true, - .builtin_module = builtin_loaded.env, - .builtin_indices = builtin_indices, + .builtin_module = builtin_modules_ptr.builtin_module.env, + .builtin_indices = builtin_modules_ptr.builtin_indices, }; const snapshots_dir = "test/snapshots"; var work_list = WorkList.init(gpa); @@ -1308,22 +1227,33 @@ fn processSnapshotContent( } } - var solver = try Check.init( - allocator, - &can_ir.types, - can_ir, - builtin_modules.items, - &module_envs, - &can_ir.store.regions, - common_idents, - ); - defer solver.deinit(); + // Use the shared type checking function to ensure identical behavior with roc check + std.debug.print("DEBUG snapshot: maybe_expr_idx = {}\n", .{maybe_expr_idx != null}); + var solver = if (maybe_expr_idx) |expr_idx| blk: { + std.debug.print("DEBUG snapshot: Using REPL path\n", .{}); + // For REPL/expr tests, use the old flow for now + var checker = try Check.init( + allocator, + &can_ir.types, + can_ir, + builtin_modules.items, + &module_envs, + &can_ir.store.regions, + common_idents, + ); + _ = try checker.checkExprRepl(expr_idx.idx); + break :blk checker; + } else blk: { + std.debug.print("DEBUG snapshot: Using FILE path (shared typeCheckModule)\n", .{}); + // For file tests, use the shared function from compile_package + // Use the SAME builtin module that was used during canonicalization + const builtin_env = config.builtin_module orelse unreachable; - if (maybe_expr_idx) |expr_idx| { - _ = try solver.checkExprRepl(expr_idx.idx); - } else { - try solver.checkFile(); - } + // Cast the slice type - we know the data is compatible + const imported_envs_const: []const *ModuleEnv = @ptrCast(builtin_modules.items); + break :blk try compile.PackageEnv.typeCheckModule(allocator, can_ir, builtin_env, imported_envs_const); + }; + defer solver.deinit(); // Assert that we have regions for every type variable solver.debugAssertArraysInSync();