Share more code

This commit is contained in:
Richard Feldman 2025-11-17 14:40:55 -05:00
parent 0c214a044c
commit 35de2e6f75
No known key found for this signature in database
3 changed files with 95 additions and 161 deletions

View file

@ -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}",

View file

@ -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);

View file

@ -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();