From 2f65dd8b89892acb9d2accdd305606a98395befc Mon Sep 17 00:00:00 2001 From: "Luke Boswell (Linux-Desktop)" Date: Thu, 19 Mar 2026 08:23:09 +1100 Subject: [PATCH 1/8] Add content-based monotype interning to deduplicate structurally identical types The monotype store was append-only with no deduplication, causing structurally identical monotypes to get different Idx values from repeated type var resolution, cross-module remapping, and independent unification paths. This led to downstream inconsistencies in caches keyed by monotype index, papered over by seven separate structural equality bandaids. Add a ContentContext-based intern map to Monotype.Store.addMonotype that hashes and compares by dereferenced span content. Remove ~200 lines of structural equality workarounds across Lower.zig, MirToLir.zig, and mir_monotype_resolver.zig. Fix remapMonotypeBetweenModulesRec to intern remapped monotypes instead of only writing to placeholder slots. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/layout/mir_monotype_resolver.zig | 25 ---- src/lir/MirToLir.zig | 85 +----------- src/mir/Lower.zig | 197 ++++----------------------- src/mir/Monotype.zig | 126 ++++++++++++++--- 4 files changed, 134 insertions(+), 299 deletions(-) diff --git a/src/layout/mir_monotype_resolver.zig b/src/layout/mir_monotype_resolver.zig index 6ed4e111086..5d02e648d82 100644 --- a/src/layout/mir_monotype_resolver.zig +++ b/src/layout/mir_monotype_resolver.zig @@ -152,11 +152,6 @@ pub const Resolver = struct { .tag_union => |tu| try self.buildTagUnionRef(self.monotype_store.getTags(tu.tags), overrides, graph, refs_by_mono), }; - if (findEquivalentMonotypeRef(self.monotype_store, mono_idx, refs_by_mono, mono_key)) |equivalent| { - try refs_by_mono.put(mono_key, equivalent); - return equivalent; - } - try refs_by_mono.put(mono_key, resolved_ref); return resolved_ref; } @@ -255,26 +250,6 @@ pub const Resolver = struct { } }; -fn findEquivalentMonotypeRef( - monotype_store: *const Monotype.Store, - mono_idx: Monotype.Idx, - refs_by_mono: *const std.AutoHashMap(u32, GraphRef), - mono_key: u32, -) ?GraphRef { - const mono = monotype_store.getMonotype(mono_idx); - - var iter = refs_by_mono.iterator(); - while (iter.next()) |entry| { - if (entry.key_ptr.* == mono_key) continue; - const other_idx: Monotype.Idx = @enumFromInt(entry.key_ptr.*); - if (std.meta.eql(monotype_store.getMonotype(other_idx), mono)) { - return entry.value_ptr.*; - } - } - - return null; -} - fn findEquivalentRootNode( allocator: Allocator, graph: *const LayoutGraph, diff --git a/src/lir/MirToLir.zig b/src/lir/MirToLir.zig index cdb9667e1e7..63cb89c14b4 100644 --- a/src/lir/MirToLir.zig +++ b/src/lir/MirToLir.zig @@ -2955,12 +2955,12 @@ fn annotationOnlyIntrinsicForFunc( const ret_ty = self.mir_store.monotype_store.getMonotype(ret_mono); if (ret_ty == .box) { - if (try self.monotypesStructurallyEqual(arg_mono, ret_ty.box.inner)) { + if (arg_mono == ret_ty.box.inner) { return .{ .op = .box_box, .result_mono = ret_mono }; } } if (arg_ty == .box) { - if (try self.monotypesStructurallyEqual(arg_ty.box.inner, ret_mono)) { + if (arg_ty.box.inner == ret_mono) { return .{ .op = .box_unbox, .result_mono = ret_mono }; } } @@ -2995,87 +2995,6 @@ fn lowerAnnotationOnlyIntrinsicCall( return try acc.finish(ll_expr, ret_layout, region); } -fn monotypesStructurallyEqual(self: *Self, lhs: Monotype.Idx, rhs: Monotype.Idx) Allocator.Error!bool { - if (lhs == rhs) return true; - var seen = std.AutoHashMap(u64, void).init(self.allocator); - defer seen.deinit(); - return try self.monotypesStructurallyEqualRec(lhs, rhs, &seen); -} - -fn monotypesStructurallyEqualRec( - self: *Self, - lhs: Monotype.Idx, - rhs: Monotype.Idx, - seen: *std.AutoHashMap(u64, void), -) Allocator.Error!bool { - if (lhs == rhs) return true; - - const lhs_u32: u32 = @intFromEnum(lhs); - const rhs_u32: u32 = @intFromEnum(rhs); - const key: u64 = (@as(u64, lhs_u32) << 32) | @as(u64, rhs_u32); - if (seen.contains(key)) return true; - try seen.put(key, {}); - - const lhs_mono = self.mir_store.monotype_store.getMonotype(lhs); - const rhs_mono = self.mir_store.monotype_store.getMonotype(rhs); - if (std.meta.activeTag(lhs_mono) != std.meta.activeTag(rhs_mono)) return false; - - return switch (lhs_mono) { - .recursive_placeholder => unreachable, - .unit => true, - .prim => |p| p == rhs_mono.prim, - .list => |l| try self.monotypesStructurallyEqualRec(l.elem, rhs_mono.list.elem, seen), - .box => |b| try self.monotypesStructurallyEqualRec(b.inner, rhs_mono.box.inner, seen), - .tuple => |t| blk: { - const lhs_elems = self.mir_store.monotype_store.getIdxSpan(t.elems); - const rhs_elems = self.mir_store.monotype_store.getIdxSpan(rhs_mono.tuple.elems); - if (lhs_elems.len != rhs_elems.len) break :blk false; - for (lhs_elems, rhs_elems) |lhs_elem, rhs_elem| { - if (!try self.monotypesStructurallyEqualRec(lhs_elem, rhs_elem, seen)) break :blk false; - } - break :blk true; - }, - .func => |f| blk: { - const rf = rhs_mono.func; - if (f.effectful != rf.effectful) break :blk false; - const lhs_args = self.mir_store.monotype_store.getIdxSpan(f.args); - const rhs_args = self.mir_store.monotype_store.getIdxSpan(rf.args); - if (lhs_args.len != rhs_args.len) break :blk false; - for (lhs_args, rhs_args) |lhs_arg, rhs_arg| { - if (!try self.monotypesStructurallyEqualRec(lhs_arg, rhs_arg, seen)) break :blk false; - } - break :blk try self.monotypesStructurallyEqualRec(f.ret, rf.ret, seen); - }, - .record => |r| blk: { - const lhs_fields = self.mir_store.monotype_store.getFields(r.fields); - const rhs_fields = self.mir_store.monotype_store.getFields(rhs_mono.record.fields); - if (lhs_fields.len != rhs_fields.len) break :blk false; - for (lhs_fields, rhs_fields) |lhs_field, rhs_field| { - if (!lhs_field.name.eql(rhs_field.name)) break :blk false; - if (!try self.monotypesStructurallyEqualRec(lhs_field.type_idx, rhs_field.type_idx, seen)) { - break :blk false; - } - } - break :blk true; - }, - .tag_union => |tu| blk: { - const lhs_tags = self.mir_store.monotype_store.getTags(tu.tags); - const rhs_tags = self.mir_store.monotype_store.getTags(rhs_mono.tag_union.tags); - if (lhs_tags.len != rhs_tags.len) break :blk false; - for (lhs_tags, rhs_tags) |lhs_tag, rhs_tag| { - if (!lhs_tag.name.eql(rhs_tag.name)) break :blk false; - const lhs_payloads = self.mir_store.monotype_store.getIdxSpan(lhs_tag.payloads); - const rhs_payloads = self.mir_store.monotype_store.getIdxSpan(rhs_tag.payloads); - if (lhs_payloads.len != rhs_payloads.len) break :blk false; - for (lhs_payloads, rhs_payloads) |lhs_payload, rhs_payload| { - if (!try self.monotypesStructurallyEqualRec(lhs_payload, rhs_payload, seen)) break :blk false; - } - } - break :blk true; - }, - }; -} - /// Follow lookups/blocks/closure origins to recover the lambda parameter span. fn resolveToLambdaParams(self: *Self, expr_id: MIR.ExprId) ?MIR.PatternSpan { const expr = self.mir_store.getExpr(expr_id); diff --git a/src/mir/Lower.zig b/src/mir/Lower.zig index bcb47284667..674f9ad6649 100644 --- a/src/mir/Lower.zig +++ b/src/mir/Lower.zig @@ -604,8 +604,12 @@ fn remapMonotypeBetweenModulesRec( .unit, .prim, .recursive_placeholder => unreachable, }; + // Intern the mapped monotype so structurally identical remapped types share indices. + const canonical = try self.store.monotype_store.addMonotype(self.allocator, mapped_mono); + // Also overwrite the placeholder slot for any recursive back-references that used it. self.store.monotype_store.monotypes.items[@intFromEnum(placeholder)] = mapped_mono; - return placeholder; + try remapped.put(monotype, canonical); + return canonical; } fn normalizeCallerMonotypeForSymbolModule( @@ -1082,15 +1086,7 @@ fn bindTypeVarMonotypesInStore( if (monotype.isNone()) return; const resolved = store_types.resolveVar(type_var); - if (bindings.get(resolved.var_)) |existing| { - if (!(try self.monotypesStructurallyEqual(existing, monotype))) { - typeBindingInvariant( - "bindTypeVarMonotypesInStore: conflicting monotype binding for type var root {d} (existing={d}, new={d})", - .{ @intFromEnum(resolved.var_), @intFromEnum(existing), @intFromEnum(monotype) }, - ); - } - return; - } + if (bindings.contains(resolved.var_)) return; switch (resolved.desc.content) { .flex, .rigid => try bindings.put(resolved.var_, monotype), @@ -2483,10 +2479,10 @@ pub fn lowerExpr(self: *Self, expr_idx: CIR.Expr.Idx) Allocator.Error!MIR.ExprId // Symbol is already lowered. Check if this is a polymorphic // specialization: the same function called with a different type. const cached_monotype = self.store.typeOf(cached_expr); - const monotype_matches_cached = try self.monotypesStructurallyEqual(cached_monotype, monotype); + const monotype_matches_cached = cached_monotype == monotype; if (!monotype.isNone() and !monotype_matches_cached) { // Check for an existing specialization with this monotype. - if (try self.lookupPolySpecialization(symbol_key, monotype)) |spec_symbol| { + if (self.lookupPolySpecialization(symbol_key, monotype)) |spec_symbol| { return try self.store.addExpr(self.allocator, .{ .lookup = spec_symbol }, monotype, region); } @@ -2548,7 +2544,7 @@ pub fn lowerExpr(self: *Self, expr_idx: CIR.Expr.Idx) Allocator.Error!MIR.ExprId const lookup_var = ModuleEnv.varFrom(lookup.pattern_idx); if (self.symbol_monotypes.get(symbol.raw())) |bound_monotype| { - const same_bound_mono = try self.monotypesStructurallyEqual(bound_monotype, monotype); + const same_bound_mono = bound_monotype == monotype; if (same_bound_mono) { return try self.store.addExpr(self.allocator, .{ .lookup = symbol }, bound_monotype, region); } @@ -2560,13 +2556,13 @@ pub fn lowerExpr(self: *Self, expr_idx: CIR.Expr.Idx) Allocator.Error!MIR.ExprId &self.type_var_seen, &self.nominal_cycle_breakers, ); - if (try self.monotypesStructurallyEqual(pattern_monotype, monotype)) { + if (pattern_monotype == monotype) { return try self.store.addExpr(self.allocator, .{ .lookup = symbol }, pattern_monotype, region); } if (self.lowered_symbols.get(symbol_key)) |cached_expr| { const cached_monotype = self.store.typeOf(cached_expr); - const monotype_matches_cached = try self.monotypesStructurallyEqual(cached_monotype, monotype); + const monotype_matches_cached = cached_monotype == monotype; if (monotype_matches_cached) { return try self.store.addExpr(self.allocator, .{ .lookup = symbol }, cached_monotype, region); } @@ -2608,9 +2604,9 @@ pub fn lowerExpr(self: *Self, expr_idx: CIR.Expr.Idx) Allocator.Error!MIR.ExprId // Symbol already lowered. Check for polymorphic specialization. if (self.lowered_symbols.get(symbol_key)) |cached_expr| { const cached_monotype = self.store.typeOf(cached_expr); - const monotype_matches_cached = try self.monotypesStructurallyEqual(cached_monotype, normalized_lookup_monotype); + const monotype_matches_cached = cached_monotype == normalized_lookup_monotype; if (!normalized_lookup_monotype.isNone() and !monotype_matches_cached) { - if (try self.lookupPolySpecialization(symbol_key, normalized_lookup_monotype)) |spec_symbol| { + if (self.lookupPolySpecialization(symbol_key, normalized_lookup_monotype)) |spec_symbol| { return try self.store.addExpr(self.allocator, .{ .lookup = spec_symbol }, monotype, region); } @@ -2631,7 +2627,7 @@ pub fn lowerExpr(self: *Self, expr_idx: CIR.Expr.Idx) Allocator.Error!MIR.ExprId if (self.lowered_symbols.get(symbol_key)) |cached_expr| { const cached_monotype = self.store.typeOf(cached_expr); - if (try self.monotypesStructurallyEqual(cached_monotype, monotype)) { + if (cached_monotype == monotype) { return try self.store.addExpr(self.allocator, .{ .lookup = symbol }, cached_monotype, region); } } @@ -3037,139 +3033,8 @@ fn polySpecKey(symbol_key: u64, monotype: Monotype.Idx) u128 { return (@as(u128, symbol_key) << 32) | @as(u128, @intFromEnum(monotype)); } -fn lookupPolySpecialization(self: *Self, symbol_key: u64, caller_monotype: Monotype.Idx) Allocator.Error!?MIR.Symbol { - const exact_key = polySpecKey(symbol_key, caller_monotype); - if (self.poly_specializations.get(exact_key)) |exact| return exact; - - // Monotype IDs are not globally canonicalized. Reuse an existing specialization - // when its cached caller monotype is structurally equal to this call site. - var it = self.poly_specializations.iterator(); - while (it.next()) |entry| { - const cache_key = entry.key_ptr.*; - const cached_symbol_key: u64 = @intCast(cache_key >> 32); - if (cached_symbol_key != symbol_key) continue; - - const cached_mono_raw: u32 = @truncate(cache_key); - const cached_mono: Monotype.Idx = @enumFromInt(cached_mono_raw); - if (try self.monotypesStructurallyEqual(cached_mono, caller_monotype)) { - return entry.value_ptr.*; - } - } - - return null; -} - -fn monotypesStructurallyEqual(self: *Self, lhs: Monotype.Idx, rhs: Monotype.Idx) Allocator.Error!bool { - if (lhs == rhs) return true; - - var seen = std.AutoHashMap(u64, void).init(self.allocator); - defer seen.deinit(); - - return try self.monotypesStructurallyEqualRec(lhs, rhs, &seen); -} - -fn monotypesStructurallyEqualRec( - self: *Self, - lhs: Monotype.Idx, - rhs: Monotype.Idx, - seen: *std.AutoHashMap(u64, void), -) Allocator.Error!bool { - if (lhs == rhs) return true; - - const lhs_u32: u32 = @intFromEnum(lhs); - const rhs_u32: u32 = @intFromEnum(rhs); - const key: u64 = (@as(u64, lhs_u32) << 32) | @as(u64, rhs_u32); - - if (seen.contains(key)) return true; - try seen.put(key, {}); - - const lhs_mono = self.store.monotype_store.getMonotype(lhs); - const rhs_mono = self.store.monotype_store.getMonotype(rhs); - if (std.meta.activeTag(lhs_mono) != std.meta.activeTag(rhs_mono)) return false; - - return switch (lhs_mono) { - .recursive_placeholder => { - if (std.debug.runtime_safety) { - std.debug.panic("recursive_placeholder survived monotype construction", .{}); - } - unreachable; - }, - .unit => true, - .prim => |lhs_prim| lhs_prim == rhs_mono.prim, - .list => |lhs_list| try self.monotypesStructurallyEqualRec(lhs_list.elem, rhs_mono.list.elem, seen), - .box => |lhs_box| try self.monotypesStructurallyEqualRec(lhs_box.inner, rhs_mono.box.inner, seen), - .tuple => |lhs_tuple| blk: { - const lhs_elem_span = lhs_tuple.elems; - const rhs_elem_span = rhs_mono.tuple.elems; - if (lhs_elem_span.len != rhs_elem_span.len) break :blk false; - for (0..lhs_elem_span.len) |i| { - const lhs_elems = self.store.monotype_store.getIdxSpan(lhs_elem_span); - const rhs_elems = self.store.monotype_store.getIdxSpan(rhs_elem_span); - const lhs_elem = lhs_elems[i]; - const rhs_elem = rhs_elems[i]; - if (!try self.monotypesStructurallyEqualRec(lhs_elem, rhs_elem, seen)) { - break :blk false; - } - } - break :blk true; - }, - .func => |lhs_func| blk: { - const rhs_func = rhs_mono.func; - if (lhs_func.effectful != rhs_func.effectful) break :blk false; - if (lhs_func.args.len != rhs_func.args.len) break :blk false; - for (0..lhs_func.args.len) |i| { - const lhs_args = self.store.monotype_store.getIdxSpan(lhs_func.args); - const rhs_args = self.store.monotype_store.getIdxSpan(rhs_func.args); - const lhs_arg = lhs_args[i]; - const rhs_arg = rhs_args[i]; - if (!try self.monotypesStructurallyEqualRec(lhs_arg, rhs_arg, seen)) { - break :blk false; - } - } - break :blk try self.monotypesStructurallyEqualRec(lhs_func.ret, rhs_func.ret, seen); - }, - .record => |lhs_record| blk: { - const lhs_field_span = lhs_record.fields; - const rhs_field_span = rhs_mono.record.fields; - if (lhs_field_span.len != rhs_field_span.len) break :blk false; - for (0..lhs_field_span.len) |i| { - const lhs_fields = self.store.monotype_store.getFields(lhs_field_span); - const rhs_fields = self.store.monotype_store.getFields(rhs_field_span); - const lhs_field = lhs_fields[i]; - const rhs_field = rhs_fields[i]; - if (!self.identsStructurallyEqual(lhs_field.name, rhs_field.name)) break :blk false; - if (!try self.monotypesStructurallyEqualRec(lhs_field.type_idx, rhs_field.type_idx, seen)) { - break :blk false; - } - } - break :blk true; - }, - .tag_union => |lhs_union| blk: { - const lhs_tag_span = lhs_union.tags; - const rhs_tag_span = rhs_mono.tag_union.tags; - if (lhs_tag_span.len != rhs_tag_span.len) break :blk false; - for (0..lhs_tag_span.len) |tag_i| { - const lhs_tags = self.store.monotype_store.getTags(lhs_tag_span); - const rhs_tags = self.store.monotype_store.getTags(rhs_tag_span); - const lhs_tag = lhs_tags[tag_i]; - const rhs_tag = rhs_tags[tag_i]; - - if (!self.identsTagNameEquivalent(lhs_tag.name, rhs_tag.name)) break :blk false; - - if (lhs_tag.payloads.len != rhs_tag.payloads.len) break :blk false; - for (0..lhs_tag.payloads.len) |payload_i| { - const lhs_payloads = self.store.monotype_store.getIdxSpan(lhs_tag.payloads); - const rhs_payloads = self.store.monotype_store.getIdxSpan(rhs_tag.payloads); - const lhs_payload = lhs_payloads[payload_i]; - const rhs_payload = rhs_payloads[payload_i]; - if (!try self.monotypesStructurallyEqualRec(lhs_payload, rhs_payload, seen)) { - break :blk false; - } - } - } - break :blk true; - }, - }; +fn lookupPolySpecialization(self: *Self, symbol_key: u64, caller_monotype: Monotype.Idx) ?MIR.Symbol { + return self.poly_specializations.get(polySpecKey(symbol_key, caller_monotype)); } /// Get the monotype for a CIR expression (via its type var). @@ -4065,7 +3930,7 @@ fn lowerBlock(self: *Self, module_env: *const ModuleEnv, block: anytype, monotyp const lowered_monotype = self.store.typeOf(lowered); const pattern_monotype = self.store.patternTypeOf(slot.pattern); - const stmt_pattern = if (try self.monotypesStructurallyEqual(pattern_monotype, lowered_monotype)) + const stmt_pattern = if (pattern_monotype == lowered_monotype) slot.pattern else try self.store.addPattern(self.allocator, .{ .bind = slot.symbol }, lowered_monotype); @@ -5280,13 +5145,9 @@ fn resolveDispatchTargetForExpr( return resolved; } -fn monotypeDispatchCompatible( - self: *Self, - expected: Monotype.Idx, - actual: Monotype.Idx, -) Allocator.Error!bool { +fn monotypeDispatchCompatible(expected: Monotype.Idx, actual: Monotype.Idx) bool { if (expected.isNone() or actual.isNone()) return true; - return try self.monotypesStructurallyEqual(expected, actual); + return expected == actual; } fn resolveDispatchTargetForDotCall( @@ -5344,7 +5205,7 @@ fn resolveDispatchTargetForDotCall( const compatible = if (fn_args.len == 0) true else - try self.monotypeDispatchCompatible(fn_args[0], receiver_monotype); + monotypeDispatchCompatible(fn_args[0], receiver_monotype); if (!compatible) continue; if (!constraint.resolved_target.isNone()) { @@ -5420,7 +5281,7 @@ fn resolveUnresolvedTypeVarDispatchTarget( self.current_module_idx, ); } - if (!try self.monotypesStructurallyEqual(candidate_mono, desired_func_monotype)) continue; + if (candidate_mono != desired_func_monotype) continue; const candidate_origin_name = candidate_env.getIdent(candidate_env.qualified_module_ident); const mapped_origin = module_env.common.findIdent(candidate_origin_name) orelse module_env.qualified_module_ident; @@ -5629,7 +5490,7 @@ fn specializeMethod(self: *Self, symbol: MIR.Symbol, caller_func_monotype: ?Mono ); const spec_key = polySpecKey(symbol_key, normalized_caller_monotype); - if (try self.lookupPolySpecialization(symbol_key, normalized_caller_monotype)) |spec_symbol| { + if (self.lookupPolySpecialization(symbol_key, normalized_caller_monotype)) |spec_symbol| { return spec_symbol; } @@ -5641,7 +5502,7 @@ fn specializeMethod(self: *Self, symbol: MIR.Symbol, caller_func_monotype: ?Mono const same_specialization = if (active_monotype.isNone()) false else - try self.monotypesStructurallyEqual(active_monotype, normalized_caller_monotype); + active_monotype == normalized_caller_monotype; if (!same_specialization) { const def_expr = self.findDefExprByMetadata(symbol_meta) orelse { @@ -5663,7 +5524,7 @@ fn specializeMethod(self: *Self, symbol: MIR.Symbol, caller_func_monotype: ?Mono if (self.lowered_symbols.get(symbol_key)) |cached_expr| { const cached_monotype = self.store.typeOf(cached_expr); - if (!try self.monotypesStructurallyEqual(cached_monotype, normalized_caller_monotype)) { + if (cached_monotype != normalized_caller_monotype) { const def_expr = self.findDefExprByMetadata(symbol_meta) orelse { if (std.debug.runtime_safety) { std.debug.panic( @@ -6136,15 +5997,7 @@ fn bindTypeVarMonotypes(self: *Self, type_var: types.Var, monotype: Monotype.Idx const resolved = self.types_store.resolveVar(type_var); - if (self.type_var_seen.get(resolved.var_)) |existing| { - if (!(try self.monotypesStructurallyEqual(existing, monotype))) { - typeBindingInvariant( - "bindTypeVarMonotypes: conflicting monotype binding for type var root {d} (existing={d}, new={d})", - .{ @intFromEnum(resolved.var_), @intFromEnum(existing), @intFromEnum(monotype) }, - ); - } - return; - } + if (self.type_var_seen.contains(resolved.var_)) return; switch (resolved.desc.content) { .flex, .rigid => { diff --git a/src/mir/Monotype.zig b/src/mir/Monotype.zig index 118c902ea24..3e0c5838b77 100644 --- a/src/mir/Monotype.zig +++ b/src/mir/Monotype.zig @@ -204,8 +204,89 @@ pub const Store = struct { /// Cached ordinary tag-union monotype for nominal Bool. bool_tag_union_idx: Idx, + intern_map: InternMap, + const prim_count = @typeInfo(Prim).@"enum".fields.len; + const ContentContext = struct { + store: *const Store, + + pub fn hash(ctx: ContentContext, mono: Monotype) u32 { + var hasher = std.hash.Wyhash.init(0); + std.hash.autoHash(&hasher, std.meta.activeTag(mono)); + + switch (mono) { + .unit, .recursive_placeholder => {}, + .prim => |p| std.hash.autoHash(&hasher, p), + .list => |l| std.hash.autoHash(&hasher, l.elem), + .box => |b| std.hash.autoHash(&hasher, b.inner), + .func => |f| { + std.hash.autoHash(&hasher, f.effectful); + std.hash.autoHash(&hasher, f.ret); + for (ctx.store.getIdxSpan(f.args)) |arg| std.hash.autoHash(&hasher, arg); + }, + .tuple => |t| { + for (ctx.store.getIdxSpan(t.elems)) |elem| std.hash.autoHash(&hasher, elem); + }, + .record => |r| { + for (ctx.store.getFields(r.fields)) |field| { + std.hash.autoHash(&hasher, field.name); + std.hash.autoHash(&hasher, field.type_idx); + } + }, + .tag_union => |tu| { + for (ctx.store.getTags(tu.tags)) |tag| { + std.hash.autoHash(&hasher, tag.name); + for (ctx.store.getIdxSpan(tag.payloads)) |p| std.hash.autoHash(&hasher, p); + } + }, + } + + return @truncate(hasher.final()); + } + + pub fn eql(ctx: ContentContext, a: Monotype, b: Monotype) bool { + const a_tag = std.meta.activeTag(a); + const b_tag = std.meta.activeTag(b); + if (a_tag != b_tag) return false; + + return switch (a) { + .unit, .recursive_placeholder => true, + .prim => |p| p == b.prim, + .list => |l| l.elem == b.list.elem, + .box => |bx| bx.inner == b.box.inner, + .func => |f| { + const bf = b.func; + if (f.effectful != bf.effectful) return false; + if (f.ret != bf.ret) return false; + return std.mem.eql(Idx, ctx.store.getIdxSpan(f.args), ctx.store.getIdxSpan(bf.args)); + }, + .tuple => |t| std.mem.eql(Idx, ctx.store.getIdxSpan(t.elems), ctx.store.getIdxSpan(b.tuple.elems)), + .record => |r| { + const a_fields = ctx.store.getFields(r.fields); + const b_fields = ctx.store.getFields(b.record.fields); + if (a_fields.len != b_fields.len) return false; + for (a_fields, b_fields) |af, bf| { + if (af.name != bf.name or af.type_idx != bf.type_idx) return false; + } + return true; + }, + .tag_union => |tu| { + const a_tags = ctx.store.getTags(tu.tags); + const b_tags = ctx.store.getTags(b.tag_union.tags); + if (a_tags.len != b_tags.len) return false; + for (a_tags, b_tags) |at, bt| { + if (at.name != bt.name) return false; + if (!std.mem.eql(Idx, ctx.store.getIdxSpan(at.payloads), ctx.store.getIdxSpan(bt.payloads))) return false; + } + return true; + }, + }; + } + }; + + const InternMap = std.HashMapUnmanaged(Monotype, Idx, ContentContext, std.hash_map.default_max_load_percentage); + pub const Scratches = struct { fields: base.Scratch(Field), tags: base.Scratch(Tag), @@ -257,32 +338,27 @@ pub const Store = struct { /// Pre-populate the store with the 16 fixed monotypes (unit + 15 primitives). pub fn init(allocator: Allocator) Allocator.Error!Store { - var monotypes: std.ArrayListUnmanaged(Monotype) = .empty; - try monotypes.ensureTotalCapacity(allocator, 1 + prim_count); - - // Unit slot - const unit_idx: Idx = @enumFromInt(monotypes.items.len); - monotypes.appendAssumeCapacity(.unit); - - // Primitive slots in enum order - var prim_idxs: [prim_count]Idx = undefined; - for (0..prim_count) |i| { - prim_idxs[i] = @enumFromInt(monotypes.items.len); - monotypes.appendAssumeCapacity(.{ .prim = @enumFromInt(i) }); - } - - return .{ - .monotypes = monotypes, + var store = Store{ + .monotypes = .empty, .extra_idx = .empty, .tags = .empty, .fields = .empty, - .unit_idx = unit_idx, - .prim_idxs = prim_idxs, + .intern_map = .{}, + .unit_idx = undefined, + .prim_idxs = undefined, .bool_tag_union_idx = .none, }; + + store.unit_idx = try store.addMonotype(allocator, .unit); + for (0..prim_count) |i| { + store.prim_idxs[i] = try store.addMonotype(allocator, .{ .prim = @enumFromInt(i) }); + } + + return store; } pub fn deinit(self: *Store, allocator: Allocator) void { + self.intern_map.deinit(allocator); self.monotypes.deinit(allocator); self.extra_idx.deinit(allocator); self.tags.deinit(allocator); @@ -290,9 +366,21 @@ pub const Store = struct { } pub fn addMonotype(self: *Store, allocator: Allocator, mono: Monotype) !Idx { + if (mono == .recursive_placeholder) { + const idx: u32 = @intCast(self.monotypes.items.len); + try self.monotypes.append(allocator, mono); + return @enumFromInt(idx); + } + + const ctx = ContentContext{ .store = self }; + const gop = try self.intern_map.getOrPutContext(allocator, mono, ctx); + if (gop.found_existing) return gop.value_ptr.*; + const idx: u32 = @intCast(self.monotypes.items.len); try self.monotypes.append(allocator, mono); - return @enumFromInt(idx); + const result: Idx = @enumFromInt(idx); + gop.value_ptr.* = result; + return result; } pub fn getMonotype(self: *const Store, idx: Idx) Monotype { From 2b00125adcf0eb1855219712f23d440f56fc6f1f Mon Sep 17 00:00:00 2001 From: "Luke Boswell (Linux-Desktop)" Date: Thu, 19 Mar 2026 13:57:02 +1100 Subject: [PATCH 2/8] Refactor monotype store to use content-based interning with resolve/TypeId API Replace the old getMonotype() tagged-union API with a resolve()-based TypeId approach across the compiler pipeline. Monotypes are now interned by structural content (internFunc, internRecord, internTagUnion, etc.) and accessed through typed accessor methods (funcArgs, funcRet, recordFields, tupleElems, etc.) instead of pattern-matching on a tagged union. Key changes: - MonotypeStore uses content-based interning for deduplication - Tag/field names use StructuralNameId from a shared name pool instead of per-module Ident.Idx, eliminating cross-module ident remapping - Callers use resolve() + kind-based switching instead of getMonotype() - @intFromEnum(mono_idx) replaced with @bitCast for new TypeId representation - Cross-module remapMonotypeBetweenModules greatly simplified since structural names are already canonical - Tests updated to use new intern* construction APIs Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cli/main.zig | 6 +- src/eval/dev_evaluator.zig | 6 +- src/layout/mir_monotype_resolver.zig | 75 +- src/lir/MirToLir.zig | 708 +++++++-------- src/mir/LambdaSet.zig | 10 +- src/mir/Lower.zig | 1237 +++++++++++--------------- src/mir/Monotype.zig | 1098 +++++++++++++---------- src/mir/test/lower_test.zig | 353 ++++---- 8 files changed, 1674 insertions(+), 1819 deletions(-) diff --git a/src/cli/main.zig b/src/cli/main.zig index ca9417712d2..3dd82f40049 100644 --- a/src/cli/main.zig +++ b/src/cli/main.zig @@ -4349,12 +4349,12 @@ fn rocBuildNative(ctx: *CliContext, args: cli_args.BuildArgs) !void { const arg_vars = platform_types.sliceVars(func.args); if (arg_vars.len == 0) { const func_mono_idx = mir_store.typeOf(mir_expr_id); - const func_mono = mir_store.monotype_store.getMonotype(func_mono_idx); - if (func_mono == .func) { + const resolved_func = mir_store.monotype_store.resolve(func_mono_idx); + if (resolved_func.kind == .func) { mir_expr_id = try mir_store.addExpr(ctx.gpa, .{ .call = .{ .func = mir_expr_id, .args = MIR.ExprSpan.empty(), - } }, func_mono.func.ret, base.Region.zero()); + } }, mir_store.monotype_store.funcRet(resolved_func), base.Region.zero()); } } } diff --git a/src/eval/dev_evaluator.zig b/src/eval/dev_evaluator.zig index 379eff3116f..1e295798869 100644 --- a/src/eval/dev_evaluator.zig +++ b/src/eval/dev_evaluator.zig @@ -893,12 +893,12 @@ pub const DevEvaluator = struct { // as calls, not as first-class function values. if (arg_layouts.len == 0) { const func_mono_idx = mir_store.typeOf(mir_expr_id); - const func_mono = mir_store.monotype_store.getMonotype(func_mono_idx); - if (func_mono == .func) { + const resolved_func = mir_store.monotype_store.resolve(func_mono_idx); + if (resolved_func.kind == .func) { mir_expr_id = mir_store.addExpr(self.allocator, .{ .call = .{ .func = mir_expr_id, .args = MIR.ExprSpan.empty(), - } }, func_mono.func.ret, base.Region.zero()) catch return error.OutOfMemory; + } }, mir_store.monotype_store.funcRet(resolved_func), base.Region.zero()) catch return error.OutOfMemory; } } diff --git a/src/layout/mir_monotype_resolver.zig b/src/layout/mir_monotype_resolver.zig index 5d02e648d82..27264f0e717 100644 --- a/src/layout/mir_monotype_resolver.zig +++ b/src/layout/mir_monotype_resolver.zig @@ -53,7 +53,7 @@ pub const Resolver = struct { overrides: ?*const std.AutoHashMap(u32, layout.Idx), ) Allocator.Error!layout.Idx { std.debug.assert(!mono_idx.isNone()); - const key = @intFromEnum(mono_idx); + const key: u32 = @bitCast(mono_idx); if (overrides) |override_map| { if (override_map.get(key)) |layout_idx| return layout_idx; @@ -99,7 +99,7 @@ pub const Resolver = struct { graph: *LayoutGraph, refs_by_mono: *std.AutoHashMap(u32, GraphRef), ) Allocator.Error!GraphRef { - const mono_key = @intFromEnum(mono_idx); + const mono_key: u32 = @bitCast(mono_idx); if (overrides) |override_map| { if (override_map.get(mono_key)) |layout_idx| { return .{ .canonical = layout_idx }; @@ -107,49 +107,54 @@ pub const Resolver = struct { } if (refs_by_mono.get(mono_key)) |cached| return cached; - const mono = self.monotype_store.getMonotype(mono_idx); - const resolved_ref: GraphRef = switch (mono) { - .recursive_placeholder => unreachable, - .unit => GraphRef{ .canonical = .zst }, - .prim => |p| GraphRef{ .canonical = switch (p) { - .str => layout.Idx.str, - .u8 => layout.Idx.u8, - .i8 => layout.Idx.i8, - .u16 => layout.Idx.u16, - .i16 => layout.Idx.i16, - .u32 => layout.Idx.u32, - .i32 => layout.Idx.i32, - .u64 => layout.Idx.u64, - .i64 => layout.Idx.i64, - .u128 => layout.Idx.u128, - .i128 => layout.Idx.i128, - .f32 => layout.Idx.f32, - .f64 => layout.Idx.f64, - .dec => layout.Idx.dec, - } }, + const resolved_id = self.monotype_store.resolve(mono_idx); + const resolved_ref: GraphRef = switch (resolved_id.kind) { + .builtin => blk: { + if (resolved_id.builtinPrim()) |p| { + break :blk GraphRef{ .canonical = switch (p) { + .str => layout.Idx.str, + .u8 => layout.Idx.u8, + .i8 => layout.Idx.i8, + .u16 => layout.Idx.u16, + .i16 => layout.Idx.i16, + .u32 => layout.Idx.u32, + .i32 => layout.Idx.i32, + .u64 => layout.Idx.u64, + .i64 => layout.Idx.i64, + .u128 => layout.Idx.u128, + .i128 => layout.Idx.i128, + .f32 => layout.Idx.f32, + .f64 => layout.Idx.f64, + .dec => layout.Idx.dec, + } }; + } + // unit + break :blk GraphRef{ .canonical = .zst }; + }, .func => blk: { const empty_captures = try self.layout_store.getEmptyRecordLayout(); break :blk GraphRef{ .canonical = try self.layout_store.insertLayout(layout.Layout.closure(empty_captures)) }; }, - .box => |b| blk: { + .box => blk: { const node_id = try graph.reserveNode(self.allocator); const local_ref = GraphRef{ .local = node_id }; try refs_by_mono.put(mono_key, local_ref); - const child_ref = try self.buildRefForMonotype(b.inner, overrides, graph, refs_by_mono); + const child_ref = try self.buildRefForMonotype(self.monotype_store.boxInner(resolved_id), overrides, graph, refs_by_mono); graph.setNode(node_id, .{ .box = child_ref }); break :blk local_ref; }, - .list => |l| blk: { + .list => blk: { const node_id = try graph.reserveNode(self.allocator); const local_ref = GraphRef{ .local = node_id }; try refs_by_mono.put(mono_key, local_ref); - const child_ref = try self.buildRefForMonotype(l.elem, overrides, graph, refs_by_mono); + const child_ref = try self.buildRefForMonotype(self.monotype_store.listElem(resolved_id), overrides, graph, refs_by_mono); graph.setNode(node_id, .{ .list = child_ref }); break :blk local_ref; }, - .record => |r| try self.buildStructFromFields(self.monotype_store.getFields(r.fields), overrides, graph, refs_by_mono), - .tuple => |t| try self.buildStructFromElems(self.monotype_store.getIdxSpan(t.elems), overrides, graph, refs_by_mono), - .tag_union => |tu| try self.buildTagUnionRef(self.monotype_store.getTags(tu.tags), overrides, graph, refs_by_mono), + .record => try self.buildStructFromFields(self.monotype_store.recordFields(resolved_id), overrides, graph, refs_by_mono), + .tuple => try self.buildStructFromElems(self.monotype_store.tupleElems(resolved_id), overrides, graph, refs_by_mono), + .tag_union => try self.buildTagUnionRef(self.monotype_store.tagUnionTags(resolved_id), overrides, graph, refs_by_mono), + .rec => unreachable, // already resolved above }; try refs_by_mono.put(mono_key, resolved_ref); @@ -158,7 +163,7 @@ pub const Resolver = struct { fn buildStructFromElems( self: *Resolver, - elems: []const Monotype.Idx, + elems: []const Monotype.TypeId, overrides: ?*const std.AutoHashMap(u32, layout.Idx), graph: *LayoutGraph, refs_by_mono: *std.AutoHashMap(u32, GraphRef), @@ -179,7 +184,7 @@ pub const Resolver = struct { fn buildStructFromFields( self: *Resolver, - fields_slice: []const Monotype.Field, + fields_slice: []const Monotype.FieldKey, overrides: ?*const std.AutoHashMap(u32, layout.Idx), graph: *LayoutGraph, refs_by_mono: *std.AutoHashMap(u32, GraphRef), @@ -192,7 +197,7 @@ pub const Resolver = struct { for (fields_slice, 0..) |field, i| { fields.appendAssumeCapacity(.{ .index = @intCast(i), - .child = try self.buildRefForMonotype(field.type_idx, overrides, graph, refs_by_mono), + .child = try self.buildRefForMonotype(field.ty, overrides, graph, refs_by_mono), }); } return self.buildStructNode(fields.items, graph); @@ -213,7 +218,7 @@ pub const Resolver = struct { fn buildTagUnionRef( self: *Resolver, - tags: []const Monotype.Tag, + tags: []const Monotype.TagKey, overrides: ?*const std.AutoHashMap(u32, layout.Idx), graph: *LayoutGraph, refs_by_mono: *std.AutoHashMap(u32, GraphRef), @@ -225,7 +230,7 @@ pub const Resolver = struct { try variants.ensureTotalCapacity(self.allocator, tags.len); for (tags) |tag| { variants.appendAssumeCapacity(try self.buildPayloadRef( - self.monotype_store.getIdxSpan(tag.payloads), + self.monotype_store.getIdxListItems(tag.payloads), overrides, graph, refs_by_mono, @@ -240,7 +245,7 @@ pub const Resolver = struct { fn buildPayloadRef( self: *Resolver, - payloads: []const Monotype.Idx, + payloads: []const Monotype.TypeId, overrides: ?*const std.AutoHashMap(u32, layout.Idx), graph: *LayoutGraph, refs_by_mono: *std.AutoHashMap(u32, GraphRef), diff --git a/src/lir/MirToLir.zig b/src/lir/MirToLir.zig index 63cb89c14b4..d3270a080bb 100644 --- a/src/lir/MirToLir.zig +++ b/src/lir/MirToLir.zig @@ -294,35 +294,28 @@ fn registerSpecializedMonotypeLayout( ) Allocator.Error!void { if (mono_idx.isNone()) return; - const monotype = self.mir_store.monotype_store.getMonotype(mono_idx); + const resolved = self.mir_store.monotype_store.resolve(mono_idx); // .func monotypes always resolve to their canonical closure layout. // They must not be overridden with a data layout from a containing // composite type, because lowerLambda/lowerHosted rely on // layoutFromMonotype returning a callable (closure) layout for the // lambda's fn_layout field. - if (monotype == .func) return; + if (resolved.kind == .func) return; - const mono_key = @intFromEnum(mono_idx); + const mono_key = @as(u32, @bitCast(mono_idx)); try self.saveMonotypeOverrideIfNeeded(saved, mono_key); - if (monotype == .func) { - // Function monotypes keep their callable layout. Specialization may refine the - // runtime value layout of a function value (e.g. empty captures -> zst), but that - // must not poison later `layoutFromMonotype(.func)` queries used for call/lambda nodes. - return; - } - if (self.specialized_monotype_layouts.get(mono_key)) |existing| { if (existing == layout_idx) return; if (builtin.mode == .Debug) { const existing_layout = self.layout_store.getLayout(existing); const new_layout = self.layout_store.getLayout(layout_idx); std.debug.panic( - "MirToLir specialized monotype layout mismatch: mono_idx={d} mono={any} existing={d}/{s} new={d}/{s}", + "MirToLir specialized monotype layout mismatch: mono_idx={d} kind={s} existing={d}/{s} new={d}/{s}", .{ mono_key, - monotype, + @tagName(resolved.kind), @intFromEnum(existing), @tagName(existing_layout.tag), @intFromEnum(layout_idx), @@ -339,30 +332,34 @@ fn registerSpecializedMonotypeLayout( self.specialized_closure_value_layout_cache.clearRetainingCapacity(); const layout_val = self.layout_store.getLayout(layout_idx); - switch (monotype) { - .recursive_placeholder, .unit, .prim => {}, + switch (resolved.kind) { + .rec => unreachable, // already resolved + .builtin => {}, .func => {}, - .box => |b| { + .box => { if (layout_val.tag == .box) { - try self.registerSpecializedMonotypeLayout(b.inner, layout_val.data.box, saved); + try self.registerSpecializedMonotypeLayout(self.mir_store.monotype_store.boxInner(resolved), layout_val.data.box, saved); } }, - .list => |l| switch (layout_val.tag) { - .list => try self.registerSpecializedMonotypeLayout(l.elem, layout_val.data.list, saved), - .list_of_zst => try self.registerSpecializedMonotypeLayout( - l.elem, - try self.zeroSizedSpecializationLayoutFromMonotype(l.elem), - saved, - ), + .list => switch (layout_val.tag) { + .list => try self.registerSpecializedMonotypeLayout(self.mir_store.monotype_store.listElem(resolved), layout_val.data.list, saved), + .list_of_zst => { + const elem = self.mir_store.monotype_store.listElem(resolved); + try self.registerSpecializedMonotypeLayout( + elem, + try self.zeroSizedSpecializationLayoutFromMonotype(elem), + saved, + ); + }, else => {}, }, - .tuple => |t| { - const elems = self.mir_store.monotype_store.getIdxSpan(t.elems); + .tuple => { + const elems = self.mir_store.monotype_store.tupleElems(resolved); if (elems.len == 0) return; if (builtin.mode == .Debug and layout_val.tag != .struct_) { std.debug.panic( - "MirToLir invariant violated: non-empty tuple monotype must specialize to struct_ layout, got mono_idx={d} mono={any} layout_idx={d} tag={s}", - .{ mono_key, monotype, @intFromEnum(layout_idx), @tagName(layout_val.tag) }, + "MirToLir invariant violated: non-empty tuple monotype must specialize to struct_ layout, got mono_idx={d} kind={s} layout_idx={d} tag={s}", + .{ mono_key, @tagName(resolved.kind), @intFromEnum(layout_idx), @tagName(layout_val.tag) }, ); } if (layout_val.tag != .struct_) return; @@ -378,13 +375,13 @@ fn registerSpecializedMonotypeLayout( } } }, - .record => |r| { - const fields = self.mir_store.monotype_store.getFields(r.fields); + .record => { + const fields = self.mir_store.monotype_store.recordFields(resolved); if (fields.len == 0) return; if (builtin.mode == .Debug and layout_val.tag != .struct_) { std.debug.panic( - "MirToLir invariant violated: non-empty record monotype must specialize to struct_ layout, got mono_idx={d} mono={any} layout_idx={d} tag={s}", - .{ mono_key, monotype, @intFromEnum(layout_idx), @tagName(layout_val.tag) }, + "MirToLir invariant violated: non-empty record monotype must specialize to struct_ layout, got mono_idx={d} kind={s} layout_idx={d} tag={s}", + .{ mono_key, @tagName(resolved.kind), @intFromEnum(layout_idx), @tagName(layout_val.tag) }, ); } if (layout_val.tag != .struct_) return; @@ -395,18 +392,18 @@ fn registerSpecializedMonotypeLayout( for (0..layout_fields.len) |li| { const layout_field = layout_fields.get(li); if (layout_field.index != semantic_index) continue; - try self.registerSpecializedMonotypeLayout(field.type_idx, layout_field.layout, saved); + try self.registerSpecializedMonotypeLayout(field.ty, layout_field.layout, saved); break; } } }, - .tag_union => |tu| { - const tags = self.mir_store.monotype_store.getTags(tu.tags); + .tag_union => { + const tags = self.mir_store.monotype_store.tagUnionTags(resolved); if (tags.len == 0) return; if (builtin.mode == .Debug and layout_val.tag != .tag_union) { std.debug.panic( - "MirToLir invariant violated: non-empty tag union monotype must specialize to tag_union layout, got mono_idx={d} mono={any} layout_idx={d} tag={s}", - .{ mono_key, monotype, @intFromEnum(layout_idx), @tagName(layout_val.tag) }, + "MirToLir invariant violated: non-empty tag union monotype must specialize to tag_union layout, got mono_idx={d} kind={s} layout_idx={d} tag={s}", + .{ mono_key, @tagName(resolved.kind), @intFromEnum(layout_idx), @tagName(layout_val.tag) }, ); } if (layout_val.tag != .tag_union) return; @@ -416,7 +413,7 @@ fn registerSpecializedMonotypeLayout( for (tags, 0..) |tag, i| { if (i >= union_layouts.len) break; const payload_layout_idx = union_layouts.get(i).payload_layout; - const payloads = self.mir_store.monotype_store.getIdxSpan(tag.payloads); + const payloads = self.mir_store.monotype_store.getIdxListItems(tag.payloads); if (payloads.len == 0) continue; const payload_layout_val = self.layout_store.getLayout(payload_layout_idx); if (payload_layout_val.tag != .struct_) continue; @@ -572,15 +569,14 @@ fn runtimeListLayoutFromExprs( } fn tagPayloadMonotypes(self: *Self, union_mono_idx: Monotype.Idx, tag_name: Ident.Idx) []const Monotype.Idx { - const union_mono = self.mir_store.monotype_store.getMonotype(union_mono_idx); - const tags = switch (union_mono) { - .tag_union => |tu| self.mir_store.monotype_store.getTags(tu.tags), - else => unreachable, - }; + const resolved = self.mir_store.monotype_store.resolve(union_mono_idx); + std.debug.assert(resolved.kind == .tag_union); + const tags = self.mir_store.monotype_store.tagUnionTags(resolved); for (tags) |tag| { - if (self.identsTextEqual(tag.name, tag_name)) { - return self.mir_store.monotype_store.getIdxSpan(tag.payloads); + const mono_tag_text = self.mir_store.monotype_store.getNameText(tag.name); + if (self.identMatchesText(tag_name, mono_tag_text)) { + return self.mir_store.monotype_store.getIdxListItems(tag.payloads); } } @@ -616,14 +612,13 @@ fn runtimeTagLayoutFromExpr( tag_data: anytype, union_mono_idx: Monotype.Idx, ) Allocator.Error!layout.Idx { - if (self.specialized_monotype_layouts.get(@intFromEnum(union_mono_idx))) |layout_idx| { + if (self.specialized_monotype_layouts.get(@as(u32, @bitCast(union_mono_idx)))) |layout_idx| { return layout_idx; } - const tags = switch (self.mir_store.monotype_store.getMonotype(union_mono_idx)) { - .tag_union => |tu| self.mir_store.monotype_store.getTags(tu.tags), - else => unreachable, - }; + const resolved = self.mir_store.monotype_store.resolve(union_mono_idx); + std.debug.assert(resolved.kind == .tag_union); + const tags = self.mir_store.monotype_store.tagUnionTags(resolved); const mir_args = self.tagPayloadExprs(union_mono_idx, tag_data.name, tag_data.args); if (tags.len == 0) return .zst; @@ -632,9 +627,10 @@ fn runtimeTagLayoutFromExpr( const save_idxs = self.scratch_layout_idxs.items.len; defer self.scratch_layout_idxs.shrinkRetainingCapacity(save_idxs); + const tag_name_text = self.identText(tag_data.name); var found_active = false; for (tags) |tag| { - if (self.identsTextEqual(tag.name, tag_data.name)) { + if (tag_name_text != null and std.mem.eql(u8, self.mir_store.monotype_store.getNameText(tag.name), tag_name_text.?)) { found_active = true; if (mir_args.len == 0) { try self.scratch_layout_idxs.append(self.allocator, zst_idx); @@ -644,7 +640,7 @@ fn runtimeTagLayoutFromExpr( continue; } - const payloads = self.mir_store.monotype_store.getIdxSpan(tag.payloads); + const payloads = self.mir_store.monotype_store.getIdxListItems(tag.payloads); if (payloads.len == 0) { try self.scratch_layout_idxs.append(self.allocator, zst_idx); } else { @@ -655,7 +651,7 @@ fn runtimeTagLayoutFromExpr( if (builtin.mode == .Debug and !found_active) { std.debug.panic( "MirToLir invariant violated: active tag ident idx {d} missing from tag union mono_idx={d}", - .{ tag_data.name.idx, @intFromEnum(union_mono_idx) }, + .{ tag_data.name.idx, @as(u32, @bitCast(union_mono_idx)) }, ); } @@ -685,8 +681,8 @@ fn runtimeLayoutFromPattern(self: *Self, mir_pat_id: MIR.PatternId) Allocator.Er return switch (pat) { .bind => |sym| blk: { - const mono = self.mir_store.monotype_store.getMonotype(mono_idx); - if (mono == .func) { + const resolved = self.mir_store.monotype_store.resolve(mono_idx); + if (resolved.kind == .func) { if (self.lambda_set_store.getSymbolLambdaSet(sym)) |ls_idx| { break :blk try self.closureValueLayoutFromLambdaSet(ls_idx); } @@ -702,7 +698,7 @@ fn runtimeLayoutFromPattern(self: *Self, mir_pat_id: MIR.PatternId) Allocator.Er .runtime_error, => self.layoutFromMonotype(mono_idx), .as_pattern => |ap| self.runtimeLayoutFromPattern(ap.pattern), - .struct_destructure => |sd| switch (self.mir_store.monotype_store.getMonotype(mono_idx)) { + .struct_destructure => |sd| switch (self.mir_store.monotype_store.resolve(mono_idx).kind) { .record => self.runtimeRecordLayoutFromPattern(mono_idx, self.mir_store.getPatternSpan(sd.fields)), .tuple => self.runtimeTupleLayoutFromPatternSpan(self.mir_store.getPatternSpan(sd.fields)), else => self.layoutFromMonotype(mono_idx), @@ -732,13 +728,10 @@ fn runtimeRecordLayoutFromPattern( mono_idx: Monotype.Idx, mir_patterns: []const MIR.PatternId, ) Allocator.Error!layout.Idx { - const mono = self.mir_store.monotype_store.getMonotype(mono_idx); - const record = switch (mono) { - .record => |r| r, - else => return self.layoutFromMonotype(mono_idx), - }; + const resolved = self.mir_store.monotype_store.resolve(mono_idx); + if (resolved.kind != .record) return self.layoutFromMonotype(mono_idx); - const all_fields = self.mir_store.monotype_store.getFields(record.fields); + const all_fields = self.mir_store.monotype_store.recordFields(resolved); if (all_fields.len == 0) return try self.layout_store.getEmptyRecordLayout(); const save_layouts = self.scratch_layouts.items.len; @@ -747,7 +740,7 @@ fn runtimeRecordLayoutFromPattern( const field_layout_idx = if (field_idx < mir_patterns.len) try self.runtimeLayoutFromPattern(mir_patterns[field_idx]) else - try self.layoutFromMonotype(field.type_idx); + try self.layoutFromMonotype(field.ty); try self.scratch_layouts.append(self.allocator, self.layout_store.getLayout(field_layout_idx)); } @@ -953,8 +946,8 @@ fn runtimeLayoutForStructField(self: *Self, expr_id: MIR.ExprId, field_idx: u32) .lookup => |symbol| blk: { if (self.symbol_layouts.get(symbol.raw())) |struct_layout| { const struct_layout_val = self.layout_store.getLayout(struct_layout); - const struct_mono = self.mir_store.monotype_store.getMonotype(self.mir_store.typeOf(expr_id)); - switch (struct_mono) { + const struct_resolved = self.mir_store.monotype_store.resolve(self.mir_store.typeOf(expr_id)); + switch (struct_resolved.kind) { .record, .tuple => {}, else => {}, } @@ -977,9 +970,9 @@ fn runtimeLayoutForStructField(self: *Self, expr_id: MIR.ExprId, field_idx: u32) /// captures payloads (or a tag union over payloads), not by callable code pointers. fn runtimeValueLayoutFromMirExpr(self: *Self, mir_expr_id: MIR.ExprId) Allocator.Error!layout.Idx { const mono_idx = self.mir_store.typeOf(mir_expr_id); - const mono = self.mir_store.monotype_store.getMonotype(mono_idx); + const resolved = self.mir_store.monotype_store.resolve(mono_idx); const expr = self.mir_store.getExpr(mir_expr_id); - if (mono == .func) { + if (resolved.kind == .func) { switch (expr) { .block => |block| return self.runtimeLayoutForBlockFinal(block), .lookup => |sym| { @@ -1047,12 +1040,12 @@ fn runtimeValueLayoutFromMirExpr(self: *Self, mir_expr_id: MIR.ExprId) Allocator return self.layoutFromMonotype(mono_idx); } return self.runtimeListLayoutFromExprs( - mono.list.elem, + self.mir_store.monotype_store.listElem(resolved), self.mir_store.getExprSpan(list_data.elems), ); }, .tag => |tag_data| { - if (mono == .tag_union) { + if (resolved.kind == .tag_union) { if (!(try self.monotypeContainsFunctionValue(mono_idx))) { return self.layoutFromMonotype(mono_idx); } @@ -1062,7 +1055,7 @@ fn runtimeValueLayoutFromMirExpr(self: *Self, mir_expr_id: MIR.ExprId) Allocator }, .run_low_level => |ll| { if (ll.op == .list_get_unsafe) { - if (self.specialized_monotype_layouts.get(@intFromEnum(mono_idx))) |layout_idx| { + if (self.specialized_monotype_layouts.get(@as(u32, @bitCast(mono_idx)))) |layout_idx| { return layout_idx; } const args = self.mir_store.getExprSpan(ll.args); @@ -1072,7 +1065,7 @@ fn runtimeValueLayoutFromMirExpr(self: *Self, mir_expr_id: MIR.ExprId) Allocator return self.runtimeListElemLayoutFromMirExpr(args[0]); } }, - .struct_ => |struct_| switch (mono) { + .struct_ => |struct_| switch (resolved.kind) { .tuple => { if (!(try self.monotypeContainsFunctionValue(mono_idx))) { return self.layoutFromMonotype(mono_idx); @@ -1087,6 +1080,7 @@ fn runtimeValueLayoutFromMirExpr(self: *Self, mir_expr_id: MIR.ExprId) Allocator }, else => {}, }, + .lookup => |sym| { if (self.symbol_layouts.get(sym.raw())) |layout_idx| { return try self.runtimeLayoutForBindingSymbol(sym, mono_idx, layout_idx); @@ -1372,11 +1366,9 @@ fn runtimeLayoutForLambdaBodyWithParamLayouts( const params = self.mir_store.getPatternSpan(resolved.params); if (params.len != param_layouts.len) return null; const fn_mono_idx = self.mir_store.typeOf(callee_expr_id); - const fn_mono = self.mir_store.monotype_store.getMonotype(fn_mono_idx); - const func_args = switch (fn_mono) { - .func => |f| self.mir_store.monotype_store.getIdxSpan(f.args), - else => return null, - }; + const fn_resolved = self.mir_store.monotype_store.resolve(fn_mono_idx); + if (fn_resolved.kind != .func) return null; + const func_args = self.mir_store.monotype_store.funcArgs(fn_resolved); if (func_args.len != param_layouts.len) return null; var saved_layouts = std.ArrayList(struct { @@ -1558,7 +1550,7 @@ fn runtimeLayoutFromSpecializedDirectCall( fn runtimeListElemLayoutFromMirExpr(self: *Self, list_mir_expr_id: MIR.ExprId) Allocator.Error!layout.Idx { const list_mono_idx = self.mir_store.typeOf(list_mir_expr_id); - const list_mono = self.mir_store.monotype_store.getMonotype(list_mono_idx); + const list_resolved = self.mir_store.monotype_store.resolve(list_mono_idx); switch (self.mir_store.getExpr(list_mir_expr_id)) { .list => |list_data| { @@ -1590,17 +1582,16 @@ fn runtimeListElemLayoutFromMirExpr(self: *Self, list_mir_expr_id: MIR.ExprId) A return switch (list_layout.tag) { .list => list_layout.data.list, - .list_of_zst => switch (list_mono) { - .list => |l| try self.zeroSizedSpecializationLayoutFromMonotype(l.elem), - else => { - if (builtin.mode == .Debug) { - std.debug.panic( - "MirToLir invariant violated: expected list monotype for list_get_unsafe source, got {s}", - .{@tagName(list_mono)}, - ); - } - unreachable; - }, + .list_of_zst => if (list_resolved.kind == .list) + try self.zeroSizedSpecializationLayoutFromMonotype(self.mir_store.monotype_store.listElem(list_resolved)) + else { + if (builtin.mode == .Debug) { + std.debug.panic( + "MirToLir invariant violated: expected list monotype for list_get_unsafe source, got {s}", + .{@tagName(list_resolved.kind)}, + ); + } + unreachable; }, else => { if (builtin.mode == .Debug) { @@ -1734,6 +1725,15 @@ fn identTextIfOwnedBy(env: anytype, ident: Ident.Idx) ?[]const u8 { return text; } +/// Resolve an Ident.Idx to its text string, searching all module envs. +fn identText(self: *const Self, ident: Ident.Idx) ?[]const u8 { + if (identTextIfOwnedBy(self.layout_store.currentEnv(), ident)) |text| return text; + for (self.layout_store.moduleEnvs()) |env| { + if (identTextIfOwnedBy(env, ident)) |text| return text; + } + return null; +} + fn identMatchesText(self: *const Self, ident: Ident.Idx, expected: []const u8) bool { if (identTextIfOwnedBy(self.layout_store.currentEnv(), ident)) |text| { if (std.mem.eql(u8, text, expected)) return true; @@ -1748,52 +1748,34 @@ fn identMatchesText(self: *const Self, ident: Ident.Idx, expected: []const u8) b return false; } -fn identsTextEqual(self: *const Self, lhs: Ident.Idx, rhs: Ident.Idx) bool { - if (lhs.eql(rhs)) return true; - - if (identTextIfOwnedBy(self.layout_store.currentEnv(), lhs)) |lhs_text| { - if (self.identMatchesText(rhs, lhs_text)) return true; - } - - for (self.layout_store.moduleEnvs()) |env| { - if (identTextIfOwnedBy(env, lhs)) |lhs_text| { - if (self.identMatchesText(rhs, lhs_text)) return true; - } - } - - return false; -} - /// Given a tag name and the monotype of the containing tag union, /// return the discriminant (sorted index of the tag name). fn tagDiscriminant(self: *const Self, tag_name: Ident.Idx, union_mono_idx: Monotype.Idx) u16 { - const monotype = self.mir_store.monotype_store.getMonotype(union_mono_idx); - switch (monotype) { - .tag_union => |tu| { - const tags = self.mir_store.monotype_store.getTags(tu.tags); - - for (tags, 0..) |tag, i| { - if (self.identsTextEqual(tag.name, tag_name)) { - return @intCast(i); - } - } - if (builtin.mode == .Debug) { - std.debug.panic( - "MirToLir invariant violated: tag ident idx {d} not found in tag union mono_idx={d}", - .{ tag_name.idx, @intFromEnum(union_mono_idx) }, - ); - } - unreachable; - }, - .prim, .unit, .record, .tuple, .list, .box, .func, .recursive_placeholder => { - if (builtin.mode == .Debug) { - std.debug.panic( - "tagDiscriminant expected tag_union; got {s} for tag ident idx {d} mono_idx={d}", - .{ @tagName(std.meta.activeTag(monotype)), tag_name.idx, @intFromEnum(union_mono_idx) }, - ); + const resolved = self.mir_store.monotype_store.resolve(union_mono_idx); + if (resolved.kind == .tag_union) { + const tags = self.mir_store.monotype_store.tagUnionTags(resolved); + + for (tags, 0..) |tag, i| { + const mono_tag_text = self.mir_store.monotype_store.getNameText(tag.name); + if (self.identMatchesText(tag_name, mono_tag_text)) { + return @intCast(i); } - unreachable; - }, + } + if (builtin.mode == .Debug) { + std.debug.panic( + "MirToLir invariant violated: tag ident idx {d} not found in tag union mono_idx={d}", + .{ tag_name.idx, @as(u32, @bitCast(union_mono_idx)) }, + ); + } + unreachable; + } else { + if (builtin.mode == .Debug) { + std.debug.panic( + "tagDiscriminant expected tag_union; got {s} for tag ident idx {d} mono_idx={d}", + .{ @tagName(resolved.kind), tag_name.idx, @as(u32, @bitCast(union_mono_idx)) }, + ); + } + unreachable; } } @@ -2158,19 +2140,32 @@ fn lowerExpr(self: *Self, mir_expr_id: MIR.ExprId) Allocator.Error!LirExprId { break :blk self.lir_store.addExpr(.{ .str_literal = lir_str_idx }, region); }, .list => |l| self.lowerList(l, mir_expr_id, region), - .struct_ => |s| switch (self.mir_store.monotype_store.getMonotype(mono_idx)) { - .record => self.lowerRecord(s.fields, mono_idx, mir_expr_id, region), - .tuple => self.lowerTuple(s.fields, mono_idx, mir_expr_id, region), - .unit => self.lowerRecord(s.fields, mono_idx, mir_expr_id, region), - else => { - if (builtin.mode == .Debug) { - std.debug.panic( - "MirToLir invariant violated: MIR struct_ has unexpected monotype {s} at expr {d}", - .{ @tagName(self.mir_store.monotype_store.getMonotype(mono_idx)), @intFromEnum(mir_expr_id) }, - ); - } - unreachable; - }, + .struct_ => |s| blk: { + const struct_resolved = self.mir_store.monotype_store.resolve(mono_idx); + break :blk switch (struct_resolved.kind) { + .record => self.lowerRecord(s.fields, mono_idx, mir_expr_id, region), + .tuple => self.lowerTuple(s.fields, mono_idx, mir_expr_id, region), + .builtin => if (struct_resolved.isUnit()) + self.lowerRecord(s.fields, mono_idx, mir_expr_id, region) + else { + if (builtin.mode == .Debug) { + std.debug.panic( + "MirToLir invariant violated: MIR struct_ has unexpected monotype kind {s} at expr {d}", + .{ @tagName(struct_resolved.kind), @intFromEnum(mir_expr_id) }, + ); + } + unreachable; + }, + else => { + if (builtin.mode == .Debug) { + std.debug.panic( + "MirToLir invariant violated: MIR struct_ has unexpected monotype kind {s} at expr {d}", + .{ @tagName(struct_resolved.kind), @intFromEnum(mir_expr_id) }, + ); + } + unreachable; + }, + }; }, .tag => |t| self.lowerTag(t, mono_idx, mir_expr_id, region), .lookup => |sym| self.lowerLookup(sym, mono_idx, mir_expr_id, region), @@ -2179,7 +2174,7 @@ fn lowerExpr(self: *Self, mir_expr_id: MIR.ExprId) Allocator.Error!LirExprId { .call => |c| self.lowerCall(c, mir_expr_id, region), .block => |b| self.lowerBlock(b, mir_expr_id, region), .borrow_scope => |b| self.lowerBorrowScope(b, mir_expr_id, region), - .struct_access => |sa| switch (self.mir_store.monotype_store.getMonotype(self.mir_store.typeOf(sa.struct_))) { + .struct_access => |sa| switch (self.mir_store.monotype_store.resolve(self.mir_store.typeOf(sa.struct_)).kind) { .record => self.lowerRecordAccess(sa.struct_, sa.field_idx, mir_expr_id, region), .tuple => self.lowerTupleAccess(sa.struct_, sa.field_idx, mir_expr_id, region), else => unreachable, @@ -2187,9 +2182,11 @@ fn lowerExpr(self: *Self, mir_expr_id: MIR.ExprId) Allocator.Error!LirExprId { .str_escape_and_quote => |s| blk: { if (builtin.mode == .Debug) { const arg_mono = self.mir_store.typeOf(s); - const arg_type = self.mir_store.monotype_store.getMonotype(arg_mono); - const ret_type = self.mir_store.monotype_store.getMonotype(mono_idx); - if (!(arg_type == .prim and arg_type.prim == .str and ret_type == .prim and ret_type.prim == .str)) { + const arg_resolved = self.mir_store.monotype_store.resolve(arg_mono); + const ret_resolved = self.mir_store.monotype_store.resolve(mono_idx); + const arg_is_str = if (arg_resolved.builtinPrim()) |p| p == .str else false; + const ret_is_str = if (ret_resolved.builtinPrim()) |p| p == .str else false; + if (!(arg_is_str and ret_is_str)) { std.debug.panic("MIR invariant violated: str_escape_and_quote must be Str -> Str", .{}); } } @@ -2565,7 +2562,7 @@ fn lowerLookup(self: *Self, sym: Symbol, mono_idx: Monotype.Idx, _: MIR.ExprId, if (self.symbol_layouts.get(sym.raw())) |binding_layout| { break :blk try self.runtimeLayoutForBindingSymbol(sym, mono_idx, binding_layout); } - if (self.mir_store.monotype_store.getMonotype(mono_idx) == .func) { + if (self.mir_store.monotype_store.resolve(mono_idx).kind == .func) { if (self.lambda_set_store.getSymbolLambdaSet(sym)) |ls_idx| { break :blk try self.closureValueLayoutFromLambdaSet(ls_idx); } @@ -2573,7 +2570,7 @@ fn lowerLookup(self: *Self, sym: Symbol, mono_idx: Monotype.Idx, _: MIR.ExprId, if (self.mir_store.getSymbolDef(sym)) |mir_def_id| { break :blk try self.runtimeValueLayoutFromMirExpr(mir_def_id); } - if (self.mir_store.monotype_store.getMonotype(mono_idx) == .func) { + if (self.mir_store.monotype_store.resolve(mono_idx).kind == .func) { break :blk try self.layoutFromMonotype(mono_idx); } break :blk try self.layoutFromMonotype(mono_idx); @@ -2680,11 +2677,9 @@ fn lowerMatch(self: *Self, match_data: anytype, mir_expr_id: MIR.ExprId, region: } fn lowerLambda(self: *Self, lam: anytype, mono_idx: Monotype.Idx, _: MIR.ExprId, region: Region) Allocator.Error!LirExprId { - const monotype = self.mir_store.monotype_store.getMonotype(mono_idx); - const func_args = switch (monotype) { - .func => |f| self.mir_store.monotype_store.getIdxSpan(f.args), - .prim, .unit, .record, .tuple, .tag_union, .list, .box, .recursive_placeholder => unreachable, - }; + const lambda_resolved = self.mir_store.monotype_store.resolve(mono_idx); + std.debug.assert(lambda_resolved.kind == .func); + const func_args = self.mir_store.monotype_store.funcArgs(lambda_resolved); const save_layouts = self.scratch_layout_idxs.items.len; defer self.scratch_layout_idxs.shrinkRetainingCapacity(save_layouts); for (func_args) |arg_mono_idx| { @@ -2710,7 +2705,8 @@ fn lowerLambdaWithParamLayouts( }; const fn_layout = try self.layoutFromMonotype(mono_idx); - const monotype = self.mir_store.monotype_store.getMonotype(mono_idx); + const lwp_resolved = self.mir_store.monotype_store.resolve(mono_idx); + std.debug.assert(lwp_resolved.kind == .func); const mir_params = self.mir_store.getPatternSpan(lam.params); if (builtin.mode == .Debug and mir_params.len != param_layouts.len) { std.debug.panic( @@ -2718,10 +2714,7 @@ fn lowerLambdaWithParamLayouts( .{ mir_params.len, param_layouts.len }, ); } - const func_args = switch (monotype) { - .func => |f| self.mir_store.monotype_store.getIdxSpan(f.args), - .prim, .unit, .record, .tuple, .tag_union, .list, .box, .recursive_placeholder => unreachable, - }; + const func_args = self.mir_store.monotype_store.funcArgs(lwp_resolved); if (builtin.mode == .Debug and func_args.len != param_layouts.len) { std.debug.panic( "MirToLir invariant violated: function monotype arg/layout mismatch ({d} args, {d} layouts)", @@ -2802,13 +2795,10 @@ fn lowerLambdaWithParamLayouts( } } } - const ret_layout = switch (monotype) { - .func => |f| blk: { - const inferred = try self.runtimeValueLayoutFromMirExpr(lam.body); - try self.registerSpecializedMonotypeLayout(f.ret, inferred, &saved_monotype_layouts); - break :blk inferred; - }, - .prim, .unit, .record, .tuple, .tag_union, .list, .box, .recursive_placeholder => unreachable, // Lambda expressions always have .func monotype + const ret_layout = blk: { + const inferred = try self.runtimeValueLayoutFromMirExpr(lam.body); + try self.registerSpecializedMonotypeLayout(self.mir_store.monotype_store.funcRet(lwp_resolved), inferred, &saved_monotype_layouts); + break :blk inferred; }; var lir_body = try self.lowerExpr(lam.body); @@ -2943,24 +2933,24 @@ fn annotationOnlyIntrinsicForFunc( self: *Self, func_mono_idx: Monotype.Idx, ) Allocator.Error!?AnnotationOnlyIntrinsic { - const func_mono = self.mir_store.monotype_store.getMonotype(func_mono_idx); - if (func_mono != .func) return null; + const func_resolved = self.mir_store.monotype_store.resolve(func_mono_idx); + if (func_resolved.kind != .func) return null; - const fn_args = self.mir_store.monotype_store.getIdxSpan(func_mono.func.args); + const fn_args = self.mir_store.monotype_store.funcArgs(func_resolved); if (fn_args.len != 1) return null; const arg_mono = fn_args[0]; - const ret_mono = func_mono.func.ret; + const ret_mono = self.mir_store.monotype_store.funcRet(func_resolved); - const arg_ty = self.mir_store.monotype_store.getMonotype(arg_mono); - const ret_ty = self.mir_store.monotype_store.getMonotype(ret_mono); + const arg_resolved = self.mir_store.monotype_store.resolve(arg_mono); + const ret_resolved = self.mir_store.monotype_store.resolve(ret_mono); - if (ret_ty == .box) { - if (arg_mono == ret_ty.box.inner) { + if (ret_resolved.kind == .box) { + if (arg_mono.eql(self.mir_store.monotype_store.boxInner(ret_resolved))) { return .{ .op = .box_box, .result_mono = ret_mono }; } } - if (arg_ty == .box) { - if (arg_ty.box.inner == ret_mono) { + if (arg_resolved.kind == .box) { + if (self.mir_store.monotype_store.boxInner(arg_resolved).eql(ret_mono)) { return .{ .op = .box_unbox, .result_mono = ret_mono }; } } @@ -3039,7 +3029,7 @@ fn adaptFunctionArgToParamLambdaSet( ) Allocator.Error!LirExprId { const param_symbol = self.functionParamSymbol(param_pat_id) orelse return arg_lir_expr; const param_mono = self.mir_store.patternTypeOf(param_pat_id); - if (self.mir_store.monotype_store.getMonotype(param_mono) != .func) return arg_lir_expr; + if (self.mir_store.monotype_store.resolve(param_mono).kind != .func) return arg_lir_expr; const param_ls_idx = self.lambda_set_store.getSymbolLambdaSet(param_symbol) orelse return arg_lir_expr; var param_members = try self.snapshotLambdaSetMembers(param_ls_idx); @@ -3664,12 +3654,12 @@ fn adaptValueLayout( ) Allocator.Error!LirExprId { if (source_layout == target_layout) return value_expr; - const monotype = self.mir_store.monotype_store.getMonotype(mono_idx); - return switch (monotype) { - .record => |record| self.adaptRecordValueLayout(value_expr, record, source_layout, target_layout, region), - .tuple => |tuple| self.adaptTupleValueLayout(value_expr, tuple, source_layout, target_layout, region), + const adapt_resolved = self.mir_store.monotype_store.resolve(mono_idx); + return switch (adapt_resolved.kind) { + .record => self.adaptRecordValueLayout(value_expr, adapt_resolved, source_layout, target_layout, region), + .tuple => self.adaptTupleValueLayout(value_expr, adapt_resolved, source_layout, target_layout, region), .func => self.adaptLayoutByStructure(value_expr, source_layout, target_layout, region), - .prim, .unit, .tag_union, .list, .box, .recursive_placeholder => value_expr, + .builtin, .tag_union, .list, .box, .rec => value_expr, }; } @@ -3857,12 +3847,12 @@ fn adaptConcreteClosureMemberPayload( fn adaptRecordValueLayout( self: *Self, value_expr: LirExprId, - record: anytype, + record_id: Monotype.TypeId, source_layout: layout.Idx, target_layout: layout.Idx, region: Region, ) Allocator.Error!LirExprId { - const fields = self.mir_store.monotype_store.getFields(record.fields); + const fields = self.mir_store.monotype_store.recordFields(record_id); if (fields.len == 0) return value_expr; const source_layout_val = self.layout_store.getLayout(source_layout); const target_layout_val = self.layout_store.getLayout(target_layout); @@ -3894,7 +3884,7 @@ fn adaptRecordValueLayout( } }, region); const adapted_field = try self.adaptValueLayout( field_expr, - fields[semantic_index].type_idx, + fields[semantic_index].ty, source_field.field_layout, target_field.layout, region, @@ -3914,12 +3904,12 @@ fn adaptRecordValueLayout( fn adaptTupleValueLayout( self: *Self, value_expr: LirExprId, - tuple: anytype, + tuple_id: Monotype.TypeId, source_layout: layout.Idx, target_layout: layout.Idx, region: Region, ) Allocator.Error!LirExprId { - const elems = self.mir_store.monotype_store.getIdxSpan(tuple.elems); + const elems = self.mir_store.monotype_store.tupleElems(tuple_id); if (elems.len == 0) return value_expr; const source_layout_val = self.layout_store.getLayout(source_layout); const target_layout_val = self.layout_store.getLayout(target_layout); @@ -4085,16 +4075,11 @@ fn lowerHosted(self: *Self, h: anytype, mono_idx: Monotype.Idx, region: Region) // as a hosted_call expression using h.index (the host function's dispatch // index). The string name is only needed for linking, handled separately. const fn_layout = try self.layoutFromMonotype(mono_idx); - const monotype = self.mir_store.monotype_store.getMonotype(mono_idx); - const ret_layout = switch (monotype) { - .func => |f| try self.layoutFromMonotype(f.ret), - .prim, .unit, .record, .tuple, .tag_union, .list, .box, .recursive_placeholder => unreachable, // Hosted expressions always have .func monotype - }; + const hosted_resolved = self.mir_store.monotype_store.resolve(mono_idx); + std.debug.assert(hosted_resolved.kind == .func); // Hosted expressions always have .func monotype + const ret_layout = try self.layoutFromMonotype(self.mir_store.monotype_store.funcRet(hosted_resolved)); - const func_args = switch (monotype) { - .func => |f| self.mir_store.monotype_store.getIdxSpan(f.args), - .prim, .unit, .record, .tuple, .tag_union, .list, .box, .recursive_placeholder => unreachable, - }; + const func_args = self.mir_store.monotype_store.funcArgs(hosted_resolved); const mir_params = self.mir_store.getPatternSpan(h.params); const save_rest_bindings = self.scratch_deferred_list_rest_bindings.items.len; defer self.scratch_deferred_list_rest_bindings.shrinkRetainingCapacity(save_rest_bindings); @@ -4272,9 +4257,9 @@ fn runtimeLayoutForBindingSymbol( fallback_layout: layout.Idx, ) Allocator.Error!layout.Idx { const existing_layout = self.symbol_layouts.get(sym.raw()); - const mono = self.mir_store.monotype_store.getMonotype(mono_idx); + const binding_resolved = self.mir_store.monotype_store.resolve(mono_idx); - if (mono != .func and !(try self.monotypeContainsFunctionValue(mono_idx))) { + if (binding_resolved.kind != .func and !(try self.monotypeContainsFunctionValue(mono_idx))) { return self.layoutFromMonotype(mono_idx); } @@ -4284,7 +4269,7 @@ fn runtimeLayoutForBindingSymbol( else false; - if (mono == .func and !has_callable_def and existing_layout == null) { + if (binding_resolved.kind == .func and !has_callable_def and existing_layout == null) { const generic_fn_layout = try self.monotype_layout_resolver.resolve(mono_idx, null); if (fallback_layout == generic_fn_layout) { if (self.lambda_set_store.getSymbolLambdaSet(sym)) |ls_idx| { @@ -4307,39 +4292,40 @@ fn monotypeContainsFunctionValueInner( mono_idx: Monotype.Idx, visited: *std.AutoHashMap(u32, void), ) Allocator.Error!bool { - const mono_key = @intFromEnum(mono_idx); + const mono_key = @as(u32, @bitCast(mono_idx)); if (visited.contains(mono_key)) return false; try visited.put(mono_key, {}); - return switch (self.mir_store.monotype_store.getMonotype(mono_idx)) { + const contains_resolved = self.mir_store.monotype_store.resolve(mono_idx); + return switch (contains_resolved.kind) { .func => true, - .record => |record| blk: { - const fields = self.mir_store.monotype_store.getFields(record.fields); + .record => blk: { + const fields = self.mir_store.monotype_store.recordFields(contains_resolved); for (fields) |field| { - if (try self.monotypeContainsFunctionValueInner(field.type_idx, visited)) break :blk true; + if (try self.monotypeContainsFunctionValueInner(field.ty, visited)) break :blk true; } break :blk false; }, - .tuple => |tuple| blk: { - const elems = self.mir_store.monotype_store.getIdxSpan(tuple.elems); + .tuple => blk: { + const elems = self.mir_store.monotype_store.tupleElems(contains_resolved); for (elems) |elem_mono| { if (try self.monotypeContainsFunctionValueInner(elem_mono, visited)) break :blk true; } break :blk false; }, - .tag_union => |tu| blk: { - const tags = self.mir_store.monotype_store.getTags(tu.tags); + .tag_union => blk: { + const tags = self.mir_store.monotype_store.tagUnionTags(contains_resolved); for (tags) |tag| { - const payloads = self.mir_store.monotype_store.getIdxSpan(tag.payloads); + const payloads = self.mir_store.monotype_store.getIdxListItems(tag.payloads); for (payloads) |payload_mono| { if (try self.monotypeContainsFunctionValueInner(payload_mono, visited)) break :blk true; } } break :blk false; }, - .list => |list| self.monotypeContainsFunctionValueInner(list.elem, visited), - .box => |box| self.monotypeContainsFunctionValueInner(box.inner, visited), - .prim, .unit, .recursive_placeholder => false, + .list => self.monotypeContainsFunctionValueInner(self.mir_store.monotype_store.listElem(contains_resolved), visited), + .box => self.monotypeContainsFunctionValueInner(self.mir_store.monotype_store.boxInner(contains_resolved), visited), + .builtin, .rec => false, }; } @@ -4474,9 +4460,9 @@ fn registerBindingPatternSymbols( .struct_destructure => |sd| { const mir_patterns = self.mir_store.getPatternSpan(sd.fields); if (mir_patterns.len == 0) return; - switch (self.mir_store.monotype_store.getMonotype(mono_idx)) { - .record => |record_mono| { - const all_fields = self.mir_store.monotype_store.getFields(record_mono.fields); + switch (self.mir_store.monotype_store.resolve(mono_idx).kind) { + .record => { + const all_fields = self.mir_store.monotype_store.recordFields(self.mir_store.monotype_store.resolve(mono_idx)); const record_layout_val = self.layout_store.getLayout(runtime_layout); if (builtin.mode == .Debug and all_fields.len != 0 and record_layout_val.tag != .struct_) { std.debug.panic( @@ -4516,10 +4502,9 @@ fn registerBindingPatternSymbols( }, .list_destructure => |ld| { const list_layout = try self.layoutFromMonotype(mono_idx); - const elem_mono = switch (self.mir_store.monotype_store.getMonotype(mono_idx)) { - .list => |list_mono| list_mono.elem, - else => unreachable, - }; + const ld_resolved = self.mir_store.monotype_store.resolve(mono_idx); + std.debug.assert(ld_resolved.kind == .list); + const elem_mono = self.mir_store.monotype_store.listElem(ld_resolved); const elem_layout = try self.layoutFromMonotype(elem_mono); const all_patterns = self.mir_store.getPatternSpan(ld.patterns); @@ -4659,9 +4644,9 @@ fn lowerPatternInternal( break :blk self.lowerWildcardBindingPattern(struct_layout, ownership_mode, region); } - switch (self.mir_store.monotype_store.getMonotype(mono_idx)) { - .record => |record_mono| { - const all_fields = self.mir_store.monotype_store.getFields(record_mono.fields); + switch (self.mir_store.monotype_store.resolve(mono_idx).kind) { + .record => { + const all_fields = self.mir_store.monotype_store.recordFields(self.mir_store.monotype_store.resolve(mono_idx)); if (all_fields.len == 0) { break :blk self.lowerWildcardBindingPattern(struct_layout, ownership_mode, region); @@ -4744,11 +4729,9 @@ fn lowerPatternInternal( }, .list_destructure => |ld| blk: { const list_layout = try self.layoutFromMonotype(mono_idx); - const list_monotype = self.mir_store.monotype_store.getMonotype(mono_idx); - const elem_layout = switch (list_monotype) { - .list => |l| try self.layoutFromMonotype(l.elem), - .prim, .unit, .record, .tuple, .tag_union, .box, .func, .recursive_placeholder => unreachable, - }; + const lp_resolved = self.mir_store.monotype_store.resolve(mono_idx); + std.debug.assert(lp_resolved.kind == .list); + const elem_layout = try self.layoutFromMonotype(self.mir_store.monotype_store.listElem(lp_resolved)); const all_patterns = self.mir_store.getPatternSpan(ld.patterns); const has_rest = !ld.rest_index.isNone(); const needs_owned_rest_discard = @@ -5146,16 +5129,11 @@ test "ANF: list of calls Let-binds each call to a symbol" { // With ANF, each call should be Let-bound to a fresh symbol, // and the list elements should be lookups to those symbols. - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; - const list_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .list = .{ .elem = i64_mono } }); + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); + const list_mono = try env.mir_store.monotype_store.internList(i64_mono); // func_args_mono: (I64) -> I64 - const func_arg_span = try env.mir_store.monotype_store.addIdxSpan(allocator, &.{i64_mono}); - const func_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .func = .{ - .args = func_arg_span, - .ret = i64_mono, - .effectful = false, - } }); + const func_mono = try env.mir_store.monotype_store.internFunc(&.{i64_mono}, i64_mono, false); const ident_f = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; const sym_f = try testMirSymbol(&env.mir_store, allocator, ident_f); @@ -5211,7 +5189,7 @@ test "MIR int literal lowers to LIR i64_literal" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); const int_expr = try env.mir_store.addExpr(allocator, .{ .int = .{ .value = .{ .bytes = @bitCast(@as(i128, 42)), .kind = .i128 }, @@ -5234,11 +5212,11 @@ test "MIR zero-arg tag lowers to LIR zero_arg_tag" { defer testDeinit(&env); // Create a single-tag union monotype: [MyTag] - const tag_name = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; - const tag_span = try env.mir_store.monotype_store.addTags(allocator, &.{ - .{ .name = tag_name, .payloads = Monotype.Span.empty() }, + const tag_name = try env.module_env.common.idents.insert(allocator, Ident.for_text("MyTag")); + const my_tag_name = try env.mir_store.monotype_store.name_pool.intern("MyTag"); + const union_mono = try env.mir_store.monotype_store.internTagUnion(&.{ + .{ .name = my_tag_name, .payloads = .empty }, }); - const union_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .tag_union = .{ .tags = tag_span } }); const tag_expr = try env.mir_store.addExpr(allocator, .{ .tag = .{ .name = tag_name, @@ -5260,8 +5238,8 @@ test "MIR empty list lowers to LIR empty_list" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; - const list_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .list = .{ .elem = i64_mono } }); + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); + const list_mono = try env.mir_store.monotype_store.internList(i64_mono); const list_expr = try env.mir_store.addExpr(allocator, .{ .list = .{ .elems = MIR.ExprSpan.empty(), @@ -5281,7 +5259,7 @@ test "MIR lookup lowers to LIR lookup" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); const ident_x = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; const sym_x = try testMirSymbol(&env.mir_store, allocator, ident_x); @@ -5303,7 +5281,7 @@ test "MIR block lowers to LIR block" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); // Create a simple block: { x = 42; x } const ident_x = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; @@ -5344,7 +5322,7 @@ test "MIR match with pattern alternatives lowers to multiple LIR match-branches" try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); // Condition: integer literal 1 const cond = try env.mir_store.addExpr(allocator, .{ .int = .{ @@ -5412,20 +5390,18 @@ test "MIR multi-tag union produces proper tag_union layout" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); // Create a 2-tag union: [Foo I64, Bar] // Tags are sorted alphabetically: Bar < Foo, so Bar=0, Foo=1 - const tag_bar = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; - const tag_foo = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 2 }; + const bar_name = try env.mir_store.monotype_store.name_pool.intern("Bar"); + const foo_name = try env.mir_store.monotype_store.name_pool.intern("Foo"); + const foo_payloads = try env.mir_store.monotype_store.internIdxList(&.{i64_mono}); - const foo_payloads = try env.mir_store.monotype_store.addIdxSpan(allocator, &.{i64_mono}); - - const tag_span = try env.mir_store.monotype_store.addTags(allocator, &.{ - .{ .name = tag_bar, .payloads = Monotype.Span.empty() }, - .{ .name = tag_foo, .payloads = foo_payloads }, + const union_mono = try env.mir_store.monotype_store.internTagUnion(&.{ + .{ .name = bar_name, .payloads = .empty }, + .{ .name = foo_name, .payloads = foo_payloads }, }); - const union_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .tag_union = .{ .tags = tag_span } }); var translator = Self.init(allocator, &env.mir_store, &env.lir_store, &env.layout_store, &env.lambda_set_store, env.module_env.idents.true_tag); defer translator.deinit(); @@ -5456,20 +5432,21 @@ test "MIR multi-tag union tags get correct discriminants" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); // Create a 2-tag union: [Bar, Foo I64] - // Sorted alphabetically by ident idx: Bar (idx=1) < Foo (idx=2) - const tag_bar = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; - const tag_foo = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 2 }; + // Sorted alphabetically: Bar < Foo + const tag_bar = try env.module_env.common.idents.insert(allocator, Ident.for_text("Bar")); + const tag_foo = try env.module_env.common.idents.insert(allocator, Ident.for_text("Foo")); - const foo_payloads = try env.mir_store.monotype_store.addIdxSpan(allocator, &.{i64_mono}); + const bar_name2 = try env.mir_store.monotype_store.name_pool.intern("Bar"); + const foo_name2 = try env.mir_store.monotype_store.name_pool.intern("Foo"); + const foo_payloads2 = try env.mir_store.monotype_store.internIdxList(&.{i64_mono}); - const tag_span = try env.mir_store.monotype_store.addTags(allocator, &.{ - .{ .name = tag_bar, .payloads = Monotype.Span.empty() }, - .{ .name = tag_foo, .payloads = foo_payloads }, + const union_mono = try env.mir_store.monotype_store.internTagUnion(&.{ + .{ .name = bar_name2, .payloads = .empty }, + .{ .name = foo_name2, .payloads = foo_payloads2 }, }); - const union_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .tag_union = .{ .tags = tag_span } }); // Lower a Bar tag (zero-arg, discriminant should be 0) const bar_expr = try env.mir_store.addExpr(allocator, .{ .tag = .{ @@ -5509,13 +5486,8 @@ test "MIR function monotype lowers to closure layout" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; - const arg_span = try env.mir_store.monotype_store.addIdxSpan(allocator, &.{i64_mono}); - const fn_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .func = .{ - .args = arg_span, - .ret = i64_mono, - .effectful = false, - } }); + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); + const fn_mono = try env.mir_store.monotype_store.internFunc(&.{i64_mono}, i64_mono, false); var translator = Self.init(allocator, &env.mir_store, &env.lir_store, &env.layout_store, &env.lambda_set_store, env.module_env.idents.true_tag); defer translator.deinit(); @@ -5530,20 +5502,17 @@ test "MIR record access finds correct field index for non-first field" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; - - // Create field name idents: a (idx=1), b (idx=2), c (idx=3) - const field_a = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; - const field_b = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 2 }; - const field_c = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 3 }; + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); // Create record monotype: { a: I64, b: I64, c: I64 } - const record_fields = try env.mir_store.monotype_store.addFields(allocator, &.{ - .{ .name = field_a, .type_idx = i64_mono }, - .{ .name = field_b, .type_idx = i64_mono }, - .{ .name = field_c, .type_idx = i64_mono }, + const name_a = try env.mir_store.monotype_store.name_pool.intern("a"); + const name_b = try env.mir_store.monotype_store.name_pool.intern("b"); + const name_c = try env.mir_store.monotype_store.name_pool.intern("c"); + const record_mono = try env.mir_store.monotype_store.internRecord(&.{ + .{ .name = name_a, .ty = i64_mono }, + .{ .name = name_b, .ty = i64_mono }, + .{ .name = name_c, .ty = i64_mono }, }); - const record_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .record = .{ .fields = record_fields } }); // Create a record literal: { a: 1, b: 2, c: 3 } const int_1 = try env.mir_store.addExpr(allocator, .{ .int = .{ @@ -5586,13 +5555,11 @@ test "MIR single-field record lowers as struct_ and preserves field access" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; - const field_only = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; - - const record_fields = try env.mir_store.monotype_store.addFields(allocator, &.{ - .{ .name = field_only, .type_idx = i64_mono }, + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); + const name_only = try env.mir_store.monotype_store.name_pool.intern("only"); + const record_mono = try env.mir_store.monotype_store.internRecord(&.{ + .{ .name = name_only, .ty = i64_mono }, }); - const record_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .record = .{ .fields = record_fields } }); const int_1 = try env.mir_store.addExpr(allocator, .{ .int = .{ .value = .{ .bytes = @bitCast(@as(i128, 1)), .kind = .i128 }, @@ -5631,12 +5598,11 @@ test "MIR tuple access preserves element index" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; - const bool_mono = try env.mir_store.monotype_store.addBoolTagUnion(allocator, env.module_env.idents); + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); + const bool_mono = try env.mir_store.monotype_store.addBoolTagUnion(); // Create tuple monotype: (I64, Bool, I64) - const tuple_elems = try env.mir_store.monotype_store.addIdxSpan(allocator, &.{ i64_mono, bool_mono, i64_mono }); - const tuple_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .tuple = .{ .elems = tuple_elems } }); + const tuple_mono = try env.mir_store.monotype_store.internTuple(&.{ i64_mono, bool_mono, i64_mono }); // Create a tuple literal: (1, true, 2) const int_1 = try env.mir_store.addExpr(allocator, .{ .int = .{ @@ -5679,9 +5645,8 @@ test "MIR single-element tuple lowers as struct_ and preserves field access" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; - const tuple_elems = try env.mir_store.monotype_store.addIdxSpan(allocator, &.{i64_mono}); - const tuple_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .tuple = .{ .elems = tuple_elems } }); + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); + const tuple_mono = try env.mir_store.monotype_store.internTuple(&.{i64_mono}); const int_1 = try env.mir_store.addExpr(allocator, .{ .int = .{ .value = .{ .bytes = @bitCast(@as(i128, 1)), .kind = .i128 }, @@ -5720,7 +5685,7 @@ test "MIR lookup propagates symbol def to LIR store" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); // Register a symbol def in MIR: x = 42 const ident_x = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; @@ -5763,15 +5728,15 @@ test "MIR single-tag union with one payload emits tag layout" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); // Create a single-tag union monotype: [Ok I64] - const tag_ok = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; - const ok_payloads = try env.mir_store.monotype_store.addIdxSpan(allocator, &.{i64_mono}); - const tag_span = try env.mir_store.monotype_store.addTags(allocator, &.{ - .{ .name = tag_ok, .payloads = ok_payloads }, + const tag_ok = try env.module_env.common.idents.insert(allocator, Ident.for_text("Ok")); + const ok_name = try env.mir_store.monotype_store.name_pool.intern("Ok"); + const ok_payloads = try env.mir_store.monotype_store.internIdxList(&.{i64_mono}); + const union_mono = try env.mir_store.monotype_store.internTagUnion(&.{ + .{ .name = ok_name, .payloads = ok_payloads }, }); - const union_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .tag_union = .{ .tags = tag_span } }); // Create expression: Ok 42 const int_42 = try env.mir_store.addExpr(allocator, .{ .int = .{ @@ -5813,12 +5778,11 @@ test "MIR single-tag union with zero args emits zero_arg_tag" { defer testDeinit(&env); // Create a single zero-arg tag union: [Unit] - const tag_unit = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; - const empty_payloads = try env.mir_store.monotype_store.addIdxSpan(allocator, &.{}); - const tag_span = try env.mir_store.monotype_store.addTags(allocator, &.{ - .{ .name = tag_unit, .payloads = empty_payloads }, + const tag_unit = try env.module_env.common.idents.insert(allocator, Ident.for_text("Unit")); + const unit_tag_name = try env.mir_store.monotype_store.name_pool.intern("Unit"); + const union_mono = try env.mir_store.monotype_store.internTagUnion(&.{ + .{ .name = unit_tag_name, .payloads = .empty }, }); - const union_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .tag_union = .{ .tags = tag_span } }); // Create expression: Unit const empty_args = try env.mir_store.addExprSpan(allocator, &.{}); @@ -5849,16 +5813,16 @@ test "MIR single-tag union with multiple payloads emits tag layout" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; - const bool_mono = try env.mir_store.monotype_store.addBoolTagUnion(allocator, env.module_env.idents); + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); + const bool_mono = try env.mir_store.monotype_store.addBoolTagUnion(); // Create a single-tag union with multiple payloads: [Pair I64 Bool] - const tag_pair = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; - const pair_payloads = try env.mir_store.monotype_store.addIdxSpan(allocator, &.{ i64_mono, bool_mono }); - const tag_span = try env.mir_store.monotype_store.addTags(allocator, &.{ - .{ .name = tag_pair, .payloads = pair_payloads }, + const tag_pair = try env.module_env.common.idents.insert(allocator, Ident.for_text("Pair")); + const pair_tag_name = try env.mir_store.monotype_store.name_pool.intern("Pair"); + const pair_payloads = try env.mir_store.monotype_store.internIdxList(&.{ i64_mono, bool_mono }); + const union_mono = try env.mir_store.monotype_store.internTagUnion(&.{ + .{ .name = pair_tag_name, .payloads = pair_payloads }, }); - const union_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .tag_union = .{ .tags = tag_span } }); // Create expression: Pair 42 true const int_42 = try env.mir_store.addExpr(allocator, .{ .int = .{ @@ -5897,18 +5861,18 @@ test "MIR single-tag union pattern with one arg preserves tag pattern" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); // Create a single-tag union monotype: [Ok I64] - const tag_ok = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; - const ok_payloads = try env.mir_store.monotype_store.addIdxSpan(allocator, &.{i64_mono}); - const tag_span = try env.mir_store.monotype_store.addTags(allocator, &.{ - .{ .name = tag_ok, .payloads = ok_payloads }, + const tag_ok = try env.module_env.common.idents.insert(allocator, Ident.for_text("Ok")); + const ok_name2 = try env.mir_store.monotype_store.name_pool.intern("Ok"); + const ok_payloads2 = try env.mir_store.monotype_store.internIdxList(&.{i64_mono}); + const union_mono = try env.mir_store.monotype_store.internTagUnion(&.{ + .{ .name = ok_name2, .payloads = ok_payloads2 }, }); - const union_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .tag_union = .{ .tags = tag_span } }); // Create pattern: Ok x (bind the payload) - const ident_x = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 2 }; + const ident_x = try env.module_env.common.idents.insert(allocator, Ident.for_text("x")); const sym_x = try testMirSymbol(&env.mir_store, allocator, ident_x); const bind_pat = try env.mir_store.addPattern(allocator, .{ .bind = sym_x }, i64_mono); const pat_args = try env.mir_store.addPatternSpan(allocator, &.{bind_pat}); @@ -5934,15 +5898,10 @@ test "MIR hosted lambda lowers to LIR lambda wrapping hosted_call" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); // Create function type: (I64, I64) -> I64 - const func_arg_span = try env.mir_store.monotype_store.addIdxSpan(allocator, &.{ i64_mono, i64_mono }); - const func_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .func = .{ - .args = func_arg_span, - .ret = i64_mono, - .effectful = false, - } }); + const func_mono = try env.mir_store.monotype_store.internFunc(&.{ i64_mono, i64_mono }, i64_mono, false); // Create parameter patterns (two binds) const ident_a = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; @@ -5999,14 +5958,9 @@ test "lambdaSetForExpr unwraps dbg_expr wrapper" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); - const func_arg_span = try env.mir_store.monotype_store.addIdxSpan(allocator, &.{i64_mono}); - const func_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .func = .{ - .args = func_arg_span, - .ret = i64_mono, - .effectful = false, - } }); + const func_mono = try env.mir_store.monotype_store.internFunc(&.{i64_mono}, i64_mono, false); const ident_arg = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; const sym_arg = try testMirSymbol(&env.mir_store, allocator, ident_arg); @@ -6045,14 +5999,9 @@ test "MIR function lookup uses symbol lambda set before wrapper def layout" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); - const func_arg_span = try env.mir_store.monotype_store.addIdxSpan(allocator, &.{i64_mono}); - const func_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .func = .{ - .args = func_arg_span, - .ret = i64_mono, - .effectful = false, - } }); + const func_mono = try env.mir_store.monotype_store.internFunc(&.{i64_mono}, i64_mono, false); const ident_arg = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; const sym_arg = try testMirSymbol(&env.mir_store, allocator, ident_arg); @@ -6098,7 +6047,7 @@ test "MIR block with decl_var and mutate_var lowers to LIR decl and mutate" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); // Build MIR: { var s = 1; s = 2; s } const ident_s = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = true }, .idx = 1 }; @@ -6153,8 +6102,8 @@ test "MIR for_loop lowers to LIR for_loop" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; - const list_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .list = .{ .elem = i64_mono } }); + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); + const list_mono = try env.mir_store.monotype_store.internList(i64_mono); const unit_mono = env.mir_store.monotype_store.unit_idx; // Build MIR: for elem in list { body } @@ -6193,8 +6142,8 @@ test "MIR while_loop lowers to LIR while_loop" { try testInitLayoutStore(&env); defer testDeinit(&env); - const bool_mono = try env.mir_store.monotype_store.addBoolTagUnion(allocator, env.module_env.idents); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const bool_mono = try env.mir_store.monotype_store.addBoolTagUnion(); + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); const unit_mono = env.mir_store.monotype_store.unit_idx; // Build MIR: while cond { body } @@ -6229,7 +6178,7 @@ test "MIR dbg_expr lowers to LIR dbg" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); // Build MIR: dbg(42) const inner_expr = try env.mir_store.addExpr(allocator, .{ .int = .{ @@ -6255,7 +6204,7 @@ test "MIR expect lowers to LIR expect" { try testInitLayoutStore(&env); defer testDeinit(&env); - const bool_mono = try env.mir_store.monotype_store.addBoolTagUnion(allocator, env.module_env.idents); + const bool_mono = try env.mir_store.monotype_store.addBoolTagUnion(); const unit_mono = env.mir_store.monotype_store.unit_idx; // Build MIR: expect (true) @@ -6304,7 +6253,7 @@ test "MIR return_expr lowers to LIR early_return" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); // Build MIR: return 42 const inner_expr = try env.mir_store.addExpr(allocator, .{ .int = .{ @@ -6352,7 +6301,7 @@ test "MIR num_plus low-level lowers to low-level" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); // Build MIR: run_low_level(.num_plus, [10, 20]) const arg0 = try env.mir_store.addExpr(allocator, .{ .int = .{ @@ -6385,8 +6334,8 @@ test "borrowed low-level temp arg lowers through explicit block binding" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; - const list_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .list = .{ .elem = i64_mono } }); + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); + const list_mono = try env.mir_store.monotype_store.internList(i64_mono); const elem0 = try env.mir_store.addExpr(allocator, .{ .int = .{ .value = .{ .bytes = @bitCast(@as(i128, 3)), .kind = .i128 }, @@ -6430,8 +6379,8 @@ test "borrowed low-level large string literal lowers through explicit block bind try testInitLayoutStore(&env); defer testDeinit(&env); - const str_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.str)]; - const u64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.u64)]; + const str_mono = env.mir_store.monotype_store.primIdx(.str); + const u64_mono = env.mir_store.monotype_store.primIdx(.u64); const large_text = "This string is deliberately longer than RocStr small-string storage"; @@ -6467,8 +6416,8 @@ test "borrow-only low-level lookup arg stays as plain low_level" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; - const list_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .list = .{ .elem = i64_mono } }); + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); + const list_mono = try env.mir_store.monotype_store.internList(i64_mono); const ident_list = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 2 }; const sym_list = try testMirSymbol(&env.mir_store, allocator, ident_list); @@ -6504,7 +6453,7 @@ test "MIR large unsigned int (U64 max) lowers to LIR i128_literal" { try testInitLayoutStore(&env); defer testDeinit(&env); - const u64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.u64)]; + const u64_mono = env.mir_store.monotype_store.primIdx(.u64); // U64 max value = 18446744073709551615, which exceeds maxInt(i64) // so it should lower to i128_literal, not i64_literal @@ -6528,21 +6477,18 @@ test "record access uses layout field order not monotype alphabetical order" { try testInitLayoutStore(&env); defer testDeinit(&env); - const u8_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.u8)]; - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; - - // Field names: age (idx=1), name (idx=2), score (idx=3) - const field_age = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; - const field_name = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 2 }; - const field_score = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 3 }; + const u8_mono = env.mir_store.monotype_store.primIdx(.u8); + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); // Monotype fields are alphabetical: { age: U8, name: I64, score: I64 } - const record_fields = try env.mir_store.monotype_store.addFields(allocator, &.{ - .{ .name = field_age, .type_idx = u8_mono }, - .{ .name = field_name, .type_idx = i64_mono }, - .{ .name = field_score, .type_idx = i64_mono }, + const name_age = try env.mir_store.monotype_store.name_pool.intern("age"); + const name_name = try env.mir_store.monotype_store.name_pool.intern("name"); + const name_score = try env.mir_store.monotype_store.name_pool.intern("score"); + const record_mono = try env.mir_store.monotype_store.internRecord(&.{ + .{ .name = name_age, .ty = u8_mono }, + .{ .name = name_name, .ty = i64_mono }, + .{ .name = name_score, .ty = i64_mono }, }); - const record_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .record = .{ .fields = record_fields } }); // Create a record literal const int_1 = try env.mir_store.addExpr(allocator, .{ .int = .{ @@ -6586,20 +6532,20 @@ test "record destructure wildcard gets actual field layout not zst" { try testInitLayoutStore(&env); defer testDeinit(&env); - const u8_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.u8)]; - const i64_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i64)]; + const u8_mono = env.mir_store.monotype_store.primIdx(.u8); + const i64_mono = env.mir_store.monotype_store.primIdx(.i64); const field_a = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 1 }; - const field_b = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 2 }; - const field_c = Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 3 }; // Record: { a: U8, b: I64, c: I64 } - const record_fields = try env.mir_store.monotype_store.addFields(allocator, &.{ - .{ .name = field_a, .type_idx = u8_mono }, - .{ .name = field_b, .type_idx = i64_mono }, - .{ .name = field_c, .type_idx = i64_mono }, + const name_a2 = try env.mir_store.monotype_store.name_pool.intern("a"); + const name_b2 = try env.mir_store.monotype_store.name_pool.intern("b"); + const name_c2 = try env.mir_store.monotype_store.name_pool.intern("c"); + const record_mono = try env.mir_store.monotype_store.internRecord(&.{ + .{ .name = name_a2, .ty = u8_mono }, + .{ .name = name_b2, .ty = i64_mono }, + .{ .name = name_c2, .ty = i64_mono }, }); - const record_mono = try env.mir_store.monotype_store.addMonotype(allocator, .{ .record = .{ .fields = record_fields } }); // Create a full-arity canonical destructure pattern that only binds field "a" (U8). // In canonical MIR order this is [a, b, c], but in layout order it becomes [b, c, a]. @@ -6645,7 +6591,7 @@ test "MIR small i128 value emits i128_literal not i64_literal" { try testInitLayoutStore(&env); defer testDeinit(&env); - const i128_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.i128)]; + const i128_mono = env.mir_store.monotype_store.primIdx(.i128); // Value 42 fits in i64, but monotype is i128. // Must emit i128_literal so LIR carries the correct 128-bit literal layout. @@ -6674,7 +6620,7 @@ test "LIR Bool match: True pattern gets discriminant 1, False body gets discrimi try testInitLayoutStore(&env); defer testDeinit(&env); - const bool_mono = try env.mir_store.monotype_store.addBoolTagUnion(allocator, env.module_env.idents); + const bool_mono = try env.mir_store.monotype_store.addBoolTagUnion(); const true_tag = env.module_env.idents.true_tag; const false_tag = env.module_env.idents.false_tag; @@ -6764,7 +6710,7 @@ test "LIR Bool match: False scrutinee gets discriminant 0" { try testInitLayoutStore(&env); defer testDeinit(&env); - const bool_mono = try env.mir_store.monotype_store.addBoolTagUnion(allocator, env.module_env.idents); + const bool_mono = try env.mir_store.monotype_store.addBoolTagUnion(); const true_tag = env.module_env.idents.true_tag; const false_tag = env.module_env.idents.false_tag; @@ -6838,7 +6784,7 @@ test "MIR small u128 value emits i128_literal not i64_literal" { try testInitLayoutStore(&env); defer testDeinit(&env); - const u128_mono = env.mir_store.monotype_store.prim_idxs[@intFromEnum(Monotype.Prim.u128)]; + const u128_mono = env.mir_store.monotype_store.primIdx(.u128); // Value 7 fits in i64, but monotype is u128. // Must emit i128_literal so LIR carries the correct 128-bit literal layout. diff --git a/src/mir/LambdaSet.zig b/src/mir/LambdaSet.zig index 5728eb6c803..32b5e37d505 100644 --- a/src/mir/LambdaSet.zig +++ b/src/mir/LambdaSet.zig @@ -591,11 +591,11 @@ fn structFieldArityForExpr(mir_store: *const MIR.Store, expr_id: MIR.ExprId) ?u3 } fn structFieldArityForMonotype(mir_store: *const MIR.Store, mono_idx: anytype) ?u32 { - const mono = mir_store.monotype_store.getMonotype(mono_idx); - return switch (mono) { - .record => |record| @intCast(mir_store.monotype_store.getFields(record.fields).len), - .tuple => |tuple| @intCast(mir_store.monotype_store.getIdxSpan(tuple.elems).len), - .box => |box| structFieldArityForMonotype(mir_store, box.inner), + const resolved = mir_store.monotype_store.resolve(mono_idx); + return switch (resolved.kind) { + .record => @intCast(mir_store.monotype_store.recordFields(resolved).len), + .tuple => @intCast(mir_store.monotype_store.tupleElems(resolved).len), + .box => structFieldArityForMonotype(mir_store, mir_store.monotype_store.boxInner(resolved)), else => null, }; } diff --git a/src/mir/Lower.zig b/src/mir/Lower.zig index 674f9ad6649..dfef7d685a5 100644 --- a/src/mir/Lower.zig +++ b/src/mir/Lower.zig @@ -310,67 +310,24 @@ fn identTextIfOwnedBy(env: *const ModuleEnv, ident: Ident.Idx) ?[]const u8 { return text; } -fn identTextForCompare(self: *const Self, ident: Ident.Idx) ?[]const u8 { - if (identTextIfOwnedBy(self.all_module_envs[self.current_module_idx], ident)) |text| return text; - return null; -} - -fn identsStructurallyEqual(self: *const Self, lhs: Ident.Idx, rhs: Ident.Idx) bool { - if (lhs.eql(rhs)) return true; - const lhs_text = self.identTextForCompare(lhs) orelse return false; - const rhs_text = self.identTextForCompare(rhs) orelse return false; - return std.mem.eql(u8, lhs_text, rhs_text); -} - fn identLastSegment(text: []const u8) []const u8 { const dot = std.mem.lastIndexOfScalar(u8, text, '.') orelse return text; return text[dot + 1 ..]; } -fn identsTagNameEquivalent(self: *const Self, lhs: Ident.Idx, rhs: Ident.Idx) bool { - if (self.identsStructurallyEqual(lhs, rhs)) return true; - - const lhs_text = self.identTextForCompare(lhs) orelse return false; - const rhs_text = self.identTextForCompare(rhs) orelse return false; - return std.mem.eql(u8, identLastSegment(lhs_text), identLastSegment(rhs_text)); -} - -fn remapIdentBetweenModules( - self: *Self, - ident: Ident.Idx, - from_module_idx: u32, - to_module_idx: u32, -) Allocator.Error!Ident.Idx { - if (from_module_idx == to_module_idx) return ident; - - const from_env = self.all_module_envs[from_module_idx]; - const to_env = self.all_module_envs[to_module_idx]; - - if (identTextIfOwnedBy(from_env, ident)) |ident_text| { - if (to_env.common.findIdent(ident_text)) |mapped| return mapped; - - // If the source monotype uses the source module's self type name, - // map that self-name directly to the target module's self-name. - if (std.mem.eql(u8, ident_text, from_env.module_name)) { - if (to_env.common.findIdent(to_env.module_name)) |target_self| return target_self; - if (to_env.common.getIdentStore().lookup(Ident.for_text(to_env.module_name))) |target_self| return target_self; - return ident; - } - - // Structural names (record fields, tag names) can be introduced by the - // caller module and legitimately be absent in the target module's ident store. - // Keep using the source ident in that case and rely on structural text comparisons. - if (to_env.common.getIdentStore().lookup(Ident.for_text(ident_text))) |mapped| return mapped; - return ident; - } - - if (std.debug.runtime_safety) { - std.debug.panic( - "remapIdentBetweenModules: source ident {d} not owned by source module {d}", - .{ ident.idx, from_module_idx }, - ); - } - unreachable; +/// Convert a StructuralNameId (from the monotype store) to an Ident.Idx +/// in the current module's ident store. Used when constructing MIR patterns +/// that require Ident.Idx names from monotype tag/field names. +fn identFromStructuralName(self: *const Self, name_id: Monotype.StructuralNameId) Ident.Idx { + const text = self.store.monotype_store.getNameText(name_id); + const module_env = self.all_module_envs[self.current_module_idx]; + if (module_env.common.findIdent(text)) |ident| return ident; + const ident_store = module_env.getIdentStoreConst(); + if (ident_store.findByString(text)) |ident| return ident; + if (ident_store.lookup(Ident.for_text(text))) |ident| return ident; + // Fallback: return a synthetic ident index; the text comparison path + // in the backend will handle it. + return Ident.Idx.NONE; } fn remapMonotypeBetweenModules( @@ -381,7 +338,7 @@ fn remapMonotypeBetweenModules( ) Allocator.Error!Monotype.Idx { if (monotype.isNone() or from_module_idx == to_module_idx) return monotype; - var remapped = std.AutoHashMap(Monotype.Idx, Monotype.Idx).init(self.allocator); + var remapped = std.AutoHashMap(u32, Monotype.Idx).init(self.allocator); defer remapped.deinit(); return self.remapMonotypeBetweenModulesRec( @@ -397,218 +354,83 @@ fn remapMonotypeBetweenModulesRec( monotype: Monotype.Idx, from_module_idx: u32, to_module_idx: u32, - remapped: *std.AutoHashMap(Monotype.Idx, Monotype.Idx), + remapped: *std.AutoHashMap(u32, Monotype.Idx), ) Allocator.Error!Monotype.Idx { if (monotype.isNone() or from_module_idx == to_module_idx) return monotype; - if (remapped.get(monotype)) |existing| return existing; - - const mono = self.store.monotype_store.getMonotype(monotype); - switch (mono) { - .unit => return self.store.monotype_store.unit_idx, - .prim => |prim| return self.store.monotype_store.primIdx(prim), - .recursive_placeholder => { - if (builtin.mode == .Debug) { - std.debug.panic("remapMonotypeBetweenModules: unexpected recursive_placeholder", .{}); - } - unreachable; - }, - .list, .box, .tuple, .func, .record, .tag_union => {}, + const mono_key: u32 = @bitCast(monotype); + if (remapped.get(mono_key)) |existing| return existing; + + const resolved = self.store.monotype_store.resolve(monotype); + switch (resolved.kind) { + .builtin => return resolved, // unit and prims are canonical + .rec => unreachable, + else => {}, } - const placeholder = try self.store.monotype_store.addMonotype(self.allocator, .recursive_placeholder); - try remapped.put(monotype, placeholder); + const rec_id = try self.store.monotype_store.reserveRecursive(); + try remapped.put(mono_key, rec_id); - const mapped_mono: Monotype.Monotype = switch (mono) { - .list => |list_mono| .{ .list = .{ - .elem = try self.remapMonotypeBetweenModulesRec( - list_mono.elem, - from_module_idx, - to_module_idx, - remapped, - ), - } }, - .box => |box_mono| .{ .box = .{ - .inner = try self.remapMonotypeBetweenModulesRec( - box_mono.inner, - from_module_idx, - to_module_idx, - remapped, - ), - } }, - .tuple => |tuple_mono| blk: { + const canonical: Monotype.Idx = switch (resolved.kind) { + .list => try self.store.monotype_store.internList( + try self.remapMonotypeBetweenModulesRec(self.store.monotype_store.listElem(resolved), from_module_idx, to_module_idx, remapped), + ), + .box => try self.store.monotype_store.internBox( + try self.remapMonotypeBetweenModulesRec(self.store.monotype_store.boxInner(resolved), from_module_idx, to_module_idx, remapped), + ), + .tuple => blk: { const idx_top = self.mono_scratches.idxs.top(); defer self.mono_scratches.idxs.clearFrom(idx_top); - - const elem_span = tuple_mono.elems; - var elem_i: u32 = 0; - while (elem_i < @as(u32, elem_span.len)) : (elem_i += 1) { - const elem_pos_u64 = @as(u64, elem_span.start) + elem_i; - if (builtin.mode == .Debug and elem_pos_u64 >= self.store.monotype_store.extra_idx.items.len) { - std.debug.panic( - "remapMonotypeBetweenModulesRec: tuple elem span out of bounds (start={d}, len={d}, i={d}, extra_len={d})", - .{ elem_span.start, elem_span.len, elem_i, self.store.monotype_store.extra_idx.items.len }, - ); - } - const elem_pos: usize = @intCast(elem_pos_u64); - const elem_mono: Monotype.Idx = @enumFromInt(self.store.monotype_store.extra_idx.items[elem_pos]); - try self.mono_scratches.idxs.append(try self.remapMonotypeBetweenModulesRec( - elem_mono, - from_module_idx, - to_module_idx, - remapped, - )); + for (self.store.monotype_store.tupleElems(resolved)) |elem| { + try self.mono_scratches.idxs.append(try self.remapMonotypeBetweenModulesRec(elem, from_module_idx, to_module_idx, remapped)); } - - const mapped_elems = try self.store.monotype_store.addIdxSpan( - self.allocator, - self.mono_scratches.idxs.sliceFromStart(idx_top), - ); - break :blk .{ .tuple = .{ .elems = mapped_elems } }; + break :blk try self.store.monotype_store.internTuple(self.mono_scratches.idxs.sliceFromStart(idx_top)); }, - .func => |func_mono| blk: { + .func => blk: { const idx_top = self.mono_scratches.idxs.top(); defer self.mono_scratches.idxs.clearFrom(idx_top); - - const arg_span = func_mono.args; - var arg_i: u32 = 0; - while (arg_i < @as(u32, arg_span.len)) : (arg_i += 1) { - const arg_pos_u64 = @as(u64, arg_span.start) + arg_i; - if (builtin.mode == .Debug and arg_pos_u64 >= self.store.monotype_store.extra_idx.items.len) { - std.debug.panic( - "remapMonotypeBetweenModulesRec: func arg span out of bounds (start={d}, len={d}, i={d}, extra_len={d})", - .{ arg_span.start, arg_span.len, arg_i, self.store.monotype_store.extra_idx.items.len }, - ); - } - const arg_pos: usize = @intCast(arg_pos_u64); - const arg_mono: Monotype.Idx = @enumFromInt(self.store.monotype_store.extra_idx.items[arg_pos]); - try self.mono_scratches.idxs.append(try self.remapMonotypeBetweenModulesRec( - arg_mono, - from_module_idx, - to_module_idx, - remapped, - )); + for (self.store.monotype_store.funcArgs(resolved)) |arg| { + try self.mono_scratches.idxs.append(try self.remapMonotypeBetweenModulesRec(arg, from_module_idx, to_module_idx, remapped)); } - const mapped_args = try self.store.monotype_store.addIdxSpan( - self.allocator, + const mapped_ret = try self.remapMonotypeBetweenModulesRec(self.store.monotype_store.funcRet(resolved), from_module_idx, to_module_idx, remapped); + break :blk try self.store.monotype_store.internFunc( self.mono_scratches.idxs.sliceFromStart(idx_top), + mapped_ret, + self.store.monotype_store.funcEffectful(resolved), ); - - const mapped_ret = try self.remapMonotypeBetweenModulesRec( - func_mono.ret, - from_module_idx, - to_module_idx, - remapped, - ); - - break :blk .{ .func = .{ - .args = mapped_args, - .ret = mapped_ret, - .effectful = func_mono.effectful, - } }; }, - .record => |record_mono| blk: { + .record => blk: { const fields_top = self.mono_scratches.fields.top(); defer self.mono_scratches.fields.clearFrom(fields_top); - - const field_span = record_mono.fields; - var field_i: u32 = 0; - while (field_i < @as(u32, field_span.len)) : (field_i += 1) { - const field_pos_u64 = @as(u64, field_span.start) + field_i; - if (builtin.mode == .Debug and field_pos_u64 >= self.store.monotype_store.fields.items.len) { - std.debug.panic( - "remapMonotypeBetweenModulesRec: record field span out of bounds (start={d}, len={d}, i={d}, fields_len={d})", - .{ field_span.start, field_span.len, field_i, self.store.monotype_store.fields.items.len }, - ); - } - const field_pos: usize = @intCast(field_pos_u64); - const field = self.store.monotype_store.fields.items[field_pos]; + for (self.store.monotype_store.recordFields(resolved)) |field| { try self.mono_scratches.fields.append(.{ - .name = try self.remapIdentBetweenModules(field.name, from_module_idx, to_module_idx), - .type_idx = try self.remapMonotypeBetweenModulesRec( - field.type_idx, - from_module_idx, - to_module_idx, - remapped, - ), + .name = field.name, // StructuralNameId is already canonical + .ty = try self.remapMonotypeBetweenModulesRec(field.ty, from_module_idx, to_module_idx, remapped), }); } - - const mapped_fields = try self.store.monotype_store.addFields( - self.allocator, - self.mono_scratches.fields.sliceFromStart(fields_top), - ); - break :blk .{ .record = .{ .fields = mapped_fields } }; + break :blk try self.store.monotype_store.internRecord(self.mono_scratches.fields.sliceFromStart(fields_top)); }, - .tag_union => |tag_union_mono| blk: { + .tag_union => blk: { const tags_top = self.mono_scratches.tags.top(); defer self.mono_scratches.tags.clearFrom(tags_top); - - const tag_span = tag_union_mono.tags; - var tag_i: u32 = 0; - while (tag_i < @as(u32, tag_span.len)) : (tag_i += 1) { - const tag_pos_u64 = @as(u64, tag_span.start) + tag_i; - if (builtin.mode == .Debug and tag_pos_u64 >= self.store.monotype_store.tags.items.len) { - std.debug.panic( - "remapMonotypeBetweenModulesRec: tag span out of bounds (start={d}, len={d}, i={d}, tags_len={d})", - .{ tag_span.start, tag_span.len, tag_i, self.store.monotype_store.tags.items.len }, - ); - } - const tag_pos: usize = @intCast(tag_pos_u64); - const tag = self.store.monotype_store.tags.items[tag_pos]; - + for (self.store.monotype_store.tagUnionTags(resolved)) |tag| { const payload_top = self.mono_scratches.idxs.top(); defer self.mono_scratches.idxs.clearFrom(payload_top); - - const payload_span = tag.payloads; - var payload_i: u32 = 0; - while (payload_i < @as(u32, payload_span.len)) : (payload_i += 1) { - const payload_pos_u64 = @as(u64, payload_span.start) + payload_i; - if (builtin.mode == .Debug and payload_pos_u64 >= self.store.monotype_store.extra_idx.items.len) { - std.debug.panic( - "remapMonotypeBetweenModulesRec: tag payload span out of bounds (start={d}, len={d}, i={d}, extra_len={d}, tag={d})", - .{ - payload_span.start, - payload_span.len, - payload_i, - self.store.monotype_store.extra_idx.items.len, - tag.name.idx, - }, - ); - } - const payload_pos: usize = @intCast(payload_pos_u64); - const payload_mono: Monotype.Idx = @enumFromInt(self.store.monotype_store.extra_idx.items[payload_pos]); - try self.mono_scratches.idxs.append(try self.remapMonotypeBetweenModulesRec( - payload_mono, - from_module_idx, - to_module_idx, - remapped, - )); + for (self.store.monotype_store.getIdxListItems(tag.payloads)) |payload| { + try self.mono_scratches.idxs.append(try self.remapMonotypeBetweenModulesRec(payload, from_module_idx, to_module_idx, remapped)); } - - const mapped_payloads = try self.store.monotype_store.addIdxSpan( - self.allocator, - self.mono_scratches.idxs.sliceFromStart(payload_top), - ); + const mapped_payloads = try self.store.monotype_store.internIdxList(self.mono_scratches.idxs.sliceFromStart(payload_top)); try self.mono_scratches.tags.append(.{ - .name = try self.remapIdentBetweenModules(tag.name, from_module_idx, to_module_idx), + .name = tag.name, // StructuralNameId is already canonical .payloads = mapped_payloads, }); } - - const mapped_tags = try self.store.monotype_store.addTags( - self.allocator, - self.mono_scratches.tags.sliceFromStart(tags_top), - ); - break :blk .{ .tag_union = .{ .tags = mapped_tags } }; + break :blk try self.store.monotype_store.internTagUnion(self.mono_scratches.tags.sliceFromStart(tags_top)); }, - .unit, .prim, .recursive_placeholder => unreachable, + .builtin, .rec => unreachable, }; - // Intern the mapped monotype so structurally identical remapped types share indices. - const canonical = try self.store.monotype_store.addMonotype(self.allocator, mapped_mono); - // Also overwrite the placeholder slot for any recursive back-references that used it. - self.store.monotype_store.monotypes.items[@intFromEnum(placeholder)] = mapped_mono; - try remapped.put(monotype, canonical); + self.store.monotype_store.finalizeRecursive(rec_id, canonical); + try remapped.put(mono_key, canonical); return canonical; } @@ -819,7 +641,7 @@ fn emitMirUnitExpr(self: *Self, region: Region) Allocator.Error!MIR.ExprId { } fn emitMirBoolLiteral(self: *Self, module_env: *const ModuleEnv, value: bool, region: Region) Allocator.Error!MIR.ExprId { - const bool_mono = try self.store.monotype_store.addBoolTagUnion(self.allocator, self.currentCommonIdents()); + const bool_mono = try self.store.monotype_store.addBoolTagUnion(); return try self.store.addExpr( self.allocator, .{ .tag = .{ @@ -1239,19 +1061,18 @@ fn bindFlatTypeMonotypesInStore( ) Allocator.Error!void { if (monotype.isNone()) return; - const mono = self.store.monotype_store.getMonotype(monotype); + const resolved = self.store.monotype_store.resolve(monotype); switch (flat_type) { .fn_pure, .fn_effectful, .fn_unbound => |func| { - const mfunc = switch (mono) { - .func => |mfunc| mfunc, - else => typeBindingInvariant( - "bindFlatTypeMonotypesInStore(fn): expected function monotype, found '{s}'", - .{@tagName(mono)}, - ), - }; + if (resolved.kind != .func) { + typeBindingInvariant( + "bindFlatTypeMonotypesInStore(fn): expected function monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } const type_args = store_types.sliceVars(func.args); - const mono_args = self.store.monotype_store.getIdxSpan(mfunc.args); + const mono_args = self.store.monotype_store.funcArgs(resolved); if (type_args.len != mono_args.len) { typeBindingInvariant( "bindFlatTypeMonotypesInStore(fn): arity mismatch (type={d}, monotype={d})", @@ -1261,20 +1082,19 @@ fn bindFlatTypeMonotypesInStore( for (type_args, 0..) |arg_var, i| { try self.bindTypeVarMonotypesInStore(store_types, common_idents, bindings, arg_var, mono_args[i]); } - try self.bindTypeVarMonotypesInStore(store_types, common_idents, bindings, func.ret, mfunc.ret); + try self.bindTypeVarMonotypesInStore(store_types, common_idents, bindings, func.ret, self.store.monotype_store.funcRet(resolved)); }, .nominal_type => |nominal| { const ident = nominal.ident.ident_idx; const origin = nominal.origin_module; if (origin.eql(common_idents.builtin_module) and ident.eql(common_idents.list)) { - const mlist = switch (mono) { - .list => |mlist| mlist, - else => typeBindingInvariant( - "bindFlatTypeMonotypesInStore(nominal List): expected list monotype, found '{s}'", - .{@tagName(mono)}, - ), - }; + if (resolved.kind != .list) { + typeBindingInvariant( + "bindFlatTypeMonotypesInStore(nominal List): expected list monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } const type_args = store_types.sliceNominalArgs(nominal); if (type_args.len != 1) { typeBindingInvariant( @@ -1282,18 +1102,17 @@ fn bindFlatTypeMonotypesInStore( .{type_args.len}, ); } - try self.bindTypeVarMonotypesInStore(store_types, common_idents, bindings, type_args[0], mlist.elem); + try self.bindTypeVarMonotypesInStore(store_types, common_idents, bindings, type_args[0], self.store.monotype_store.listElem(resolved)); return; } if (origin.eql(common_idents.builtin_module) and ident.eql(common_idents.box)) { - const mbox = switch (mono) { - .box => |mbox| mbox, - else => typeBindingInvariant( - "bindFlatTypeMonotypesInStore(nominal Box): expected box monotype, found '{s}'", - .{@tagName(mono)}, - ), - }; + if (resolved.kind != .box) { + typeBindingInvariant( + "bindFlatTypeMonotypesInStore(nominal Box): expected box monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } const type_args = store_types.sliceNominalArgs(nominal); if (type_args.len != 1) { typeBindingInvariant( @@ -1301,17 +1120,16 @@ fn bindFlatTypeMonotypesInStore( .{type_args.len}, ); } - try self.bindTypeVarMonotypesInStore(store_types, common_idents, bindings, type_args[0], mbox.inner); + try self.bindTypeVarMonotypesInStore(store_types, common_idents, bindings, type_args[0], self.store.monotype_store.boxInner(resolved)); return; } if (origin.eql(common_idents.builtin_module) and builtinPrimForNominal(ident, common_idents) != null) { - switch (mono) { - .prim => {}, - else => typeBindingInvariant( - "bindFlatTypeMonotypesInStore(nominal prim): expected prim monotype, found '{s}'", - .{@tagName(mono)}, - ), + if (resolved.kind != .builtin or resolved.builtinPrim() == null) { + typeBindingInvariant( + "bindFlatTypeMonotypesInStore(nominal prim): expected prim monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); } return; } @@ -1325,14 +1143,13 @@ fn bindFlatTypeMonotypesInStore( ); }, .record => |record| { - const mrec = switch (mono) { - .record => |mrec| mrec, - else => typeBindingInvariant( - "bindFlatTypeMonotypesInStore(record): expected record monotype, found '{s}'", - .{@tagName(mono)}, - ), - }; - const mono_fields = self.store.monotype_store.getFields(mrec.fields); + if (resolved.kind != .record) { + typeBindingInvariant( + "bindFlatTypeMonotypesInStore(record): expected record monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } + const mono_fields = self.store.monotype_store.recordFields(resolved); var seen_field_indices: std.ArrayListUnmanaged(u32) = .empty; defer seen_field_indices.deinit(self.allocator); @@ -1344,7 +1161,7 @@ fn bindFlatTypeMonotypesInStore( for (field_names, field_vars) |field_name, field_var| { const field_idx = self.recordFieldIndexByName(field_name, mono_fields); try appendSeenIndex(self.allocator, &seen_field_indices, field_idx); - try self.bindTypeVarMonotypesInStore(store_types, common_idents, bindings, field_var, mono_fields[field_idx].type_idx); + try self.bindTypeVarMonotypesInStore(store_types, common_idents, bindings, field_var, mono_fields[field_idx].ty); } var ext_var = current_row.ext; @@ -1367,7 +1184,7 @@ fn bindFlatTypeMonotypesInStore( for (ext_names, ext_vars) |field_name, field_var| { const field_idx = self.recordFieldIndexByName(field_name, mono_fields); try appendSeenIndex(self.allocator, &seen_field_indices, field_idx); - try self.bindTypeVarMonotypesInStore(store_types, common_idents, bindings, field_var, mono_fields[field_idx].type_idx); + try self.bindTypeVarMonotypesInStore(store_types, common_idents, bindings, field_var, mono_fields[field_idx].ty); } break :rows; }, @@ -1387,32 +1204,30 @@ fn bindFlatTypeMonotypesInStore( } }, .record_unbound => |fields_range| { - const mrec = switch (mono) { - .record => |mrec| mrec, - else => typeBindingInvariant( - "bindFlatTypeMonotypesInStore(record_unbound): expected record monotype, found '{s}'", - .{@tagName(mono)}, - ), - }; - const mono_fields = self.store.monotype_store.getFields(mrec.fields); + if (resolved.kind != .record) { + typeBindingInvariant( + "bindFlatTypeMonotypesInStore(record_unbound): expected record monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } + const mono_fields = self.store.monotype_store.recordFields(resolved); const fields_slice = store_types.getRecordFieldsSlice(fields_range); const field_names = fields_slice.items(.name); const field_vars = fields_slice.items(.var_); for (field_names, field_vars) |field_name, field_var| { const field_idx = self.recordFieldIndexByName(field_name, mono_fields); - try self.bindTypeVarMonotypesInStore(store_types, common_idents, bindings, field_var, mono_fields[field_idx].type_idx); + try self.bindTypeVarMonotypesInStore(store_types, common_idents, bindings, field_var, mono_fields[field_idx].ty); } }, .tuple => |tuple| { - const mtup = switch (mono) { - .tuple => |mtup| mtup, - else => typeBindingInvariant( - "bindFlatTypeMonotypesInStore(tuple): expected tuple monotype, found '{s}'", - .{@tagName(mono)}, - ), - }; + if (resolved.kind != .tuple) { + typeBindingInvariant( + "bindFlatTypeMonotypesInStore(tuple): expected tuple monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } const elem_vars = store_types.sliceVars(tuple.elems); - const elem_monos = self.store.monotype_store.getIdxSpan(mtup.elems); + const elem_monos = self.store.monotype_store.tupleElems(resolved); if (elem_vars.len != elem_monos.len) { typeBindingInvariant( "bindFlatTypeMonotypesInStore(tuple): arity mismatch (type={d}, monotype={d})", @@ -1424,14 +1239,13 @@ fn bindFlatTypeMonotypesInStore( } }, .tag_union => |tag_union| { - const mtag = switch (mono) { - .tag_union => |mtag| mtag, - else => typeBindingInvariant( - "bindFlatTypeMonotypesInStore(tag_union): expected tag union monotype, found '{s}'", - .{@tagName(mono)}, - ), - }; - const mono_tags = self.store.monotype_store.getTags(mtag.tags); + if (resolved.kind != .tag_union) { + typeBindingInvariant( + "bindFlatTypeMonotypesInStore(tag_union): expected tag union monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } + const mono_tags = self.store.monotype_store.tagUnionTags(resolved); var seen_tag_indices: std.ArrayListUnmanaged(u32) = .empty; defer seen_tag_indices.deinit(self.allocator); @@ -1475,19 +1289,21 @@ fn bindFlatTypeMonotypesInStore( } } }, - .empty_record => switch (mono) { - .unit => {}, - else => typeBindingInvariant( - "bindFlatTypeMonotypesInStore(empty_record): expected unit monotype, found '{s}'", - .{@tagName(mono)}, - ), + .empty_record => { + if (!resolved.isUnit() and resolved.kind != .record) { + typeBindingInvariant( + "bindFlatTypeMonotypesInStore(empty_record): expected unit monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } }, - .empty_tag_union => switch (mono) { - .tag_union => {}, - else => typeBindingInvariant( - "bindFlatTypeMonotypesInStore(empty_tag_union): expected tag union monotype, found '{s}'", - .{@tagName(mono)}, - ), + .empty_tag_union => { + if (resolved.kind != .tag_union) { + typeBindingInvariant( + "bindFlatTypeMonotypesInStore(empty_tag_union): expected tag union monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } }, } } @@ -1529,16 +1345,15 @@ fn lowerStrInspektNominal( } const outer_mono = try self.monotypeFromTypeVarInEnv(type_env, type_var); - const box_mono = self.store.monotype_store.getMonotype(outer_mono); - const box_data = switch (box_mono) { - .box => |box| box, - else => typeBindingInvariant( - "lowerStrInspektNominal(Box): expected box monotype, found '{s}'", - .{@tagName(box_mono)}, - ), - }; + const box_resolved = self.store.monotype_store.resolve(outer_mono); + if (box_resolved.kind != .box) { + typeBindingInvariant( + "lowerStrInspektNominal(Box): expected box monotype, found kind={d}", + .{@intFromEnum(box_resolved.kind)}, + ); + } - const unbox_fn_mono = try self.buildFuncMonotype(&.{outer_mono}, box_data.inner, false); + const unbox_fn_mono = try self.buildFuncMonotype(&.{outer_mono}, self.store.monotype_store.boxInner(box_resolved), false); const unbox_target: ResolvedDispatchTarget = .{ .origin = common.builtin_module, .method_ident = common.builtin_box_unbox, @@ -1557,7 +1372,7 @@ fn lowerStrInspektNominal( const unboxed = try self.store.addExpr( self.allocator, .{ .call = .{ .func = func_expr, .args = unbox_args } }, - box_data.inner, + self.store.monotype_store.boxInner(box_resolved), region, ); @@ -1602,13 +1417,14 @@ fn lowerStrInspektNominal( return self.lowerStrInspektExpr(type_env, value_expr, type_env.types.getNominalBackingVar(nominal), region); } - const method_func = switch (self.store.monotype_store.getMonotype(method_func_mono)) { - .func => |func| func, - else => typeBindingInvariant( - "lowerStrInspektNominal: expected function monotype for to_inspect, found '{s}'", - .{@tagName(self.store.monotype_store.getMonotype(method_func_mono))}, - ), - }; + const method_func_resolved = self.store.monotype_store.resolve(method_func_mono); + if (method_func_resolved.kind != .func) { + typeBindingInvariant( + "lowerStrInspektNominal: expected function monotype for to_inspect, found kind={d}", + .{@intFromEnum(method_func_resolved.kind)}, + ); + } + const method_ret = self.store.monotype_store.funcRet(method_func_resolved); const lowered_method_symbol = try self.specializeMethod(method_info.symbol, method_func_mono); const func_expr = try self.store.addExpr( @@ -1621,13 +1437,13 @@ fn lowerStrInspektNominal( const call_expr = try self.store.addExpr( self.allocator, .{ .call = .{ .func = func_expr, .args = call_args } }, - method_func.ret, + method_ret, region, ); - const ret_mono = self.store.monotype_store.getMonotype(method_func.ret); - if (ret_mono == .prim and ret_mono.prim == .str) { - return call_expr; + const ret_resolved = self.store.monotype_store.resolve(method_ret); + if (ret_resolved.builtinPrim()) |p| { + if (p == .str) return call_expr; } return self.lowerStrInspektExpr(method_info.target_env, call_expr, resolved_func.func.ret, region); @@ -1938,7 +1754,7 @@ fn lowerStrInspektList( region: Region, ) Allocator.Error!MIR.ExprId { const str_mono = self.store.monotype_store.primIdx(.str); - const bool_mono = try self.store.monotype_store.addBoolTagUnion(self.allocator, self.currentCommonIdents()); + const bool_mono = try self.store.monotype_store.addBoolTagUnion(); const unit_mono = self.store.monotype_store.unit_idx; const elem_mono = try self.monotypeFromTypeVarInEnv(type_env, elem_var); const current_env = self.all_module_envs[self.current_module_idx]; @@ -2016,19 +1832,20 @@ fn lowerStrInspektExprByMonotype( mono_idx: Monotype.Idx, region: Region, ) Allocator.Error!MIR.ExprId { - const mono = self.store.monotype_store.getMonotype(mono_idx); - return switch (mono) { - .prim => |prim| switch (prim) { - .str => blk: { - break :blk try self.store.addExpr( - self.allocator, - .{ .str_escape_and_quote = value_expr }, - self.store.monotype_store.primIdx(.str), - region, - ); - }, - else => |p| blk: { - const ll = toStrLowLevelForPrim(p) orelse unreachable; + const resolved = self.store.monotype_store.resolve(mono_idx); + return switch (resolved.kind) { + .builtin => blk: { + if (resolved.isUnit()) break :blk self.emitMirStrLiteral("{}", region); + if (resolved.builtinPrim()) |prim| { + if (prim == .str) { + break :blk try self.store.addExpr( + self.allocator, + .{ .str_escape_and_quote = value_expr }, + self.store.monotype_store.primIdx(.str), + region, + ); + } + const ll = toStrLowLevelForPrim(prim) orelse unreachable; const args = try self.store.addExprSpan(self.allocator, &.{value_expr}); break :blk try self.store.addExpr( self.allocator, @@ -2036,20 +1853,20 @@ fn lowerStrInspektExprByMonotype( self.store.monotype_store.primIdx(.str), region, ); - }, + } + unreachable; }, - .record => |record| self.lowerStrInspektRecordByMonotype(module_env, value_expr, record, region), - .tuple => |tup| self.lowerStrInspektTupleByMonotype(module_env, value_expr, tup, region), - .tag_union => |tu| self.lowerStrInspektTagUnionByMonotype(module_env, value_expr, tu, mono_idx, region), - .list => |list_data| self.lowerStrInspektListByMonotype(module_env, value_expr, list_data, region), - .unit => self.emitMirStrLiteral("{}", region), - .box => |box_data| blk: { + .record => self.lowerStrInspektRecordByMonotype(module_env, value_expr, resolved, region), + .tuple => self.lowerStrInspektTupleByMonotype(module_env, value_expr, resolved, region), + .tag_union => self.lowerStrInspektTagUnionByMonotype(module_env, value_expr, resolved, mono_idx, region), + .list => self.lowerStrInspektListByMonotype(module_env, value_expr, resolved, region), + .box => blk: { const common = self.currentCommonIdents(); - const unbox_fn_mono = try self.buildFuncMonotype(&.{mono_idx}, box_data.inner, false); + const box_inner = self.store.monotype_store.boxInner(resolved); + const unbox_fn_mono = try self.buildFuncMonotype(&.{mono_idx}, box_inner, false); const unbox_target: ResolvedDispatchTarget = .{ .origin = common.builtin_module, .method_ident = common.builtin_box_unbox, - // resolvedDispatchTargetToSymbol only uses origin/method_ident. .fn_var = undefined, }; @@ -2065,19 +1882,19 @@ fn lowerStrInspektExprByMonotype( const unboxed = try self.store.addExpr( self.allocator, .{ .call = .{ .func = func_expr, .args = unbox_args } }, - box_data.inner, + box_inner, region, ); - const inner_str = try self.lowerStrInspektExprByMonotype(module_env, unboxed, box_data.inner, region); + const inner_str = try self.lowerStrInspektExprByMonotype(module_env, unboxed, box_inner, region); const open = try self.emitMirStrLiteral("Box(", region); const close = try self.emitMirStrLiteral(")", region); break :blk self.foldMirStrConcat(&.{ open, inner_str, close }, region); }, .func => self.emitMirStrLiteral("", region), - .recursive_placeholder => { + .rec => { if (std.debug.runtime_safety) { - std.debug.panic("recursive_placeholder survived monotype construction", .{}); + std.debug.panic("recursive type survived monotype construction", .{}); } unreachable; }, @@ -2086,32 +1903,31 @@ fn lowerStrInspektExprByMonotype( fn lowerStrInspektRecordByMonotype( self: *Self, - module_env: *const ModuleEnv, + _: *const ModuleEnv, value_expr: MIR.ExprId, record: anytype, region: Region, ) Allocator.Error!MIR.ExprId { - const field_span = record.fields; - const fields = self.store.monotype_store.getFields(field_span); + const fields = self.store.monotype_store.recordFields(record); if (fields.len == 0) return self.emitMirStrLiteral("{}", region); const save_exprs = self.scratch_expr_ids.top(); defer self.scratch_expr_ids.clearFrom(save_exprs); try self.scratch_expr_ids.append(try self.emitMirStrLiteral("{ ", region)); - for (0..field_span.len) |i| { - const current_fields = self.store.monotype_store.getFields(field_span); + for (0..fields.len) |i| { + const current_fields = self.store.monotype_store.recordFields(record); const field = current_fields[i]; if (i > 0) { try self.scratch_expr_ids.append(try self.emitMirStrLiteral(", ", region)); } - const field_name = module_env.getIdent(field.name); + const field_name = self.store.monotype_store.getNameText(field.name); const label = try std.fmt.allocPrint(self.allocator, "{s}: ", .{field_name}); defer self.allocator.free(label); try self.scratch_expr_ids.append(try self.emitMirStrLiteral(label, region)); - const field_expr = try self.emitMirStructAccess(value_expr, @intCast(i), field.type_idx, region); - try self.scratch_expr_ids.append(try self.lowerStrInspektExprByMonotype(module_env, field_expr, field.type_idx, region)); + const field_expr = try self.emitMirStructAccess(value_expr, @intCast(i), field.ty, region); + try self.scratch_expr_ids.append(try self.lowerStrInspektExprByMonotype(self.all_module_envs[self.current_module_idx], field_expr, field.ty, region)); } try self.scratch_expr_ids.append(try self.emitMirStrLiteral(" }", region)); return self.foldMirStrConcat(self.scratch_expr_ids.sliceFromStart(save_exprs), region); @@ -2124,16 +1940,15 @@ fn lowerStrInspektTupleByMonotype( tup: anytype, region: Region, ) Allocator.Error!MIR.ExprId { - const elem_span = tup.elems; - const elems = self.store.monotype_store.getIdxSpan(elem_span); + const elems = self.store.monotype_store.tupleElems(tup); if (elems.len == 0) return self.emitMirStrLiteral("()", region); const save_exprs = self.scratch_expr_ids.top(); defer self.scratch_expr_ids.clearFrom(save_exprs); try self.scratch_expr_ids.append(try self.emitMirStrLiteral("(", region)); - for (0..elem_span.len) |i| { - const current_elems = self.store.monotype_store.getIdxSpan(elem_span); + for (0..elems.len) |i| { + const current_elems = self.store.monotype_store.tupleElems(tup); const elem_mono = current_elems[i]; if (i > 0) { try self.scratch_expr_ids.append(try self.emitMirStrLiteral(", ", region)); @@ -2154,35 +1969,34 @@ fn lowerStrInspektTagUnionByMonotype( mono_idx: Monotype.Idx, region: Region, ) Allocator.Error!MIR.ExprId { - const tag_span = tu.tags; - const tags = self.store.monotype_store.getTags(tag_span); + const tags = self.store.monotype_store.tagUnionTags(tu); if (tags.len == 0) return self.emitMirStrLiteral("", region); const save_branches = self.scratch_branches.top(); defer self.scratch_branches.clearFrom(save_branches); - for (0..tag_span.len) |tag_i| { - const current_tags = self.store.monotype_store.getTags(tag_span); + for (0..tags.len) |tag_i| { + const current_tags = self.store.monotype_store.tagUnionTags(tu); const tag = current_tags[tag_i]; - const payload_span = tag.payloads; - const payloads = self.store.monotype_store.getIdxSpan(payload_span); + const payloads = self.store.monotype_store.getIdxListItems(tag.payloads); const save_payload_patterns = self.scratch_pattern_ids.top(); defer self.scratch_pattern_ids.clearFrom(save_payload_patterns); const save_payload_symbols = self.scratch_captures.top(); defer self.scratch_captures.clearFrom(save_payload_symbols); - for (self.store.monotype_store.getIdxSpan(payload_span)) |payload_mono| { + for (payloads) |payload_mono| { const bind = try self.makeSyntheticBind(payload_mono, false); try self.scratch_pattern_ids.append(bind.pattern); try self.scratch_captures.append(.{ .symbol = bind.symbol }); } const payload_pattern_span = try self.store.addPatternSpan(self.allocator, self.scratch_pattern_ids.sliceFromStart(save_payload_patterns)); - const tag_args = try self.wrapMultiPayloadTagPatterns(tag.name, mono_idx, payload_pattern_span); + const tag_ident = self.identFromStructuralName(tag.name); + const tag_args = try self.wrapMultiPayloadTagPatterns(tag_ident, mono_idx, payload_pattern_span); const tag_pattern = try self.store.addPattern( self.allocator, - .{ .tag = .{ .name = tag.name, .args = tag_args } }, + .{ .tag = .{ .name = tag_ident, .args = tag_args } }, mono_idx, ); const branch_patterns = try self.store.addBranchPatterns(self.allocator, &.{MIR.BranchPattern{ @@ -2190,19 +2004,20 @@ fn lowerStrInspektTagUnionByMonotype( .degenerate = false, }}); + const tag_name_text = self.store.monotype_store.getNameText(tag.name); const body = if (payloads.len == 0) blk: { - break :blk try self.emitMirStrLiteral(module_env.getIdent(tag.name), region); + break :blk try self.emitMirStrLiteral(tag_name_text, region); } else blk: { const save_parts = self.scratch_expr_ids.top(); defer self.scratch_expr_ids.clearFrom(save_parts); - const tag_open = try std.fmt.allocPrint(self.allocator, "{s}(", .{module_env.getIdent(tag.name)}); + const tag_open = try std.fmt.allocPrint(self.allocator, "{s}(", .{tag_name_text}); defer self.allocator.free(tag_open); try self.scratch_expr_ids.append(try self.emitMirStrLiteral(tag_open, region)); const payload_symbols = self.scratch_captures.sliceFromStart(save_payload_symbols); - for (0..payload_span.len) |i| { - const payload_mono = self.store.monotype_store.getIdxSpan(payload_span)[i]; + for (0..payloads.len) |i| { + const payload_mono = self.store.monotype_store.getIdxListItems(tag.payloads)[i]; const payload_capture = payload_symbols[i]; if (i > 0) { try self.scratch_expr_ids.append(try self.emitMirStrLiteral(", ", region)); @@ -2241,12 +2056,12 @@ fn lowerStrInspektListByMonotype( region: Region, ) Allocator.Error!MIR.ExprId { const str_mono = self.store.monotype_store.primIdx(.str); - const bool_mono = try self.store.monotype_store.addBoolTagUnion(self.allocator, self.currentCommonIdents()); + const bool_mono = try self.store.monotype_store.addBoolTagUnion(); const unit_mono = self.store.monotype_store.unit_idx; const acc_bind = try self.makeSyntheticBind(str_mono, true); const first_bind = try self.makeSyntheticBind(bool_mono, true); - const elem_bind = try self.makeSyntheticBind(list_data.elem, false); + const elem_bind = try self.makeSyntheticBind(self.store.monotype_store.listElem(list_data), false); const open_bracket = try self.emitMirStrLiteral("[", region); const close_bracket = try self.emitMirStrLiteral("]", region); @@ -2261,8 +2076,8 @@ fn lowerStrInspektListByMonotype( // acc = acc ++ prefix ++ inspect(elem) const first_lookup = try self.emitMirLookup(first_bind.symbol, bool_mono, region); const prefix = try self.createBoolMatch(module_env, first_lookup, empty, comma, str_mono, region); - const elem_lookup = try self.emitMirLookup(elem_bind.symbol, list_data.elem, region); - const elem_inspected = try self.lowerStrInspektExprByMonotype(module_env, elem_lookup, list_data.elem, region); + const elem_lookup = try self.emitMirLookup(elem_bind.symbol, self.store.monotype_store.listElem(list_data), region); + const elem_inspected = try self.lowerStrInspektExprByMonotype(module_env, elem_lookup, self.store.monotype_store.listElem(list_data), region); const prefixed_elem = try self.emitMirStrConcat(prefix, elem_inspected, region); const acc_lookup = try self.emitMirLookup(acc_bind.symbol, str_mono, region); const new_acc = try self.emitMirStrConcat(acc_lookup, prefixed_elem, region); @@ -2352,9 +2167,9 @@ pub fn lowerExpr(self: *Self, expr_idx: CIR.Expr.Idx) Allocator.Error!MIR.ExprId .e_typed_int => |ti| try self.store.addExpr(self.allocator, .{ .int = .{ .value = ti.value } }, monotype, region), .e_typed_frac => |tf| { const roc_dec = builtins.dec.RocDec{ .num = tf.value.toI128() }; - const mono = self.store.monotype_store.getMonotype(monotype); - return switch (mono) { - .prim => |p| switch (p) { + const resolved_mono = self.store.monotype_store.resolve(monotype); + if (resolved_mono.builtinPrim()) |p| { + return switch (p) { .f64 => try self.store.addExpr(self.allocator, .{ .frac_f64 = roc_dec.toF64() }, monotype, region), .f32 => try self.store.addExpr(self.allocator, .{ .frac_f32 = @floatCast(roc_dec.toF64()) }, monotype, region), .dec => try self.store.addExpr(self.allocator, .{ .dec = roc_dec }, monotype, region), @@ -2368,18 +2183,17 @@ pub fn lowerExpr(self: *Self, expr_idx: CIR.Expr.Idx) Allocator.Error!MIR.ExprId } unreachable; }, - }, - else => { - if (std.debug.runtime_safety) { - const type_name = module_env.getIdent(tf.type_name); - std.debug.panic( - "lowerExpr(e_typed_frac): non-prim monotype for type '{s}' (checker/lowering invariant broken)", - .{type_name}, - ); - } - unreachable; - }, - }; + }; + } else { + if (std.debug.runtime_safety) { + const type_name = module_env.getIdent(tf.type_name); + std.debug.panic( + "lowerExpr(e_typed_frac): non-prim monotype for type '{s}' (checker/lowering invariant broken)", + .{type_name}, + ); + } + unreachable; + } }, // --- Strings --- @@ -2989,7 +2803,7 @@ fn hoistAnonymousFunctionExpr(self: *Self, expr: MIR.ExprId) Allocator.Error!MIR /// stable symbol identity before lambda-set inference runs. Named lookups and /// lifted closures already have that identity; anonymous lambda expressions do not. fn stabilizeEscapingFunctionExpr(self: *Self, expr: MIR.ExprId) Allocator.Error!MIR.ExprId { - if (self.store.monotype_store.getMonotype(self.store.typeOf(expr)) != .func) return expr; + if (self.store.monotype_store.resolve(self.store.typeOf(expr)).kind != .func) return expr; if (self.store.getExprClosureMember(expr) != null) return expr; return switch (self.store.getExpr(expr)) { @@ -3030,7 +2844,7 @@ fn isLambdaExpr(mir_store: *const MIR.Store, expr_id: MIR.ExprId) bool { /// Compute the composite cache key for polymorphic specializations. fn polySpecKey(symbol_key: u64, monotype: Monotype.Idx) u128 { - return (@as(u128, symbol_key) << 32) | @as(u128, @intFromEnum(monotype)); + return (@as(u128, symbol_key) << 32) | @as(u128, @as(u32, @bitCast(monotype))); } fn lookupPolySpecialization(self: *Self, symbol_key: u64, caller_monotype: Monotype.Idx) ?MIR.Symbol { @@ -3055,12 +2869,7 @@ fn currentCommonIdents(self: *const Self) ModuleEnv.CommonIdents { /// Build a function monotype from argument types, return type, and effectfulness. fn buildFuncMonotype(self: *Self, arg_monotypes: []const Monotype.Idx, ret: Monotype.Idx, effectful: bool) Allocator.Error!Monotype.Idx { - const args_span = try self.store.monotype_store.addIdxSpan(self.allocator, arg_monotypes); - return try self.store.monotype_store.addMonotype(self.allocator, .{ .func = .{ - .args = args_span, - .ret = ret, - .effectful = effectful, - } }); + return try self.store.monotype_store.internFunc(arg_monotypes, ret, effectful); } /// Lower a CIR Expr.Span to an MIR ExprSpan. @@ -3176,19 +2985,20 @@ fn lowerPattern(self: *Self, module_env: *const ModuleEnv, pattern_idx: CIR.Patt const cir_destructs = module_env.store.sliceRecordDestructs(record_pat.destructs); const pats_top = self.scratch_pattern_ids.top(); defer self.scratch_pattern_ids.clearFrom(pats_top); - const mono_field_span = switch (self.store.monotype_store.getMonotype(monotype)) { - .record => |record_mono| record_mono.fields, - .unit => Monotype.FieldSpan.empty(), - else => typeBindingInvariant( - "lowerPattern(record_destructure): expected record monotype, found '{s}'", - .{@tagName(self.store.monotype_store.getMonotype(monotype))}, - ), - }; - const mono_fields_for_defaults = self.store.monotype_store.getFields(mono_field_span); + const mono_resolved = self.store.monotype_store.resolve(monotype); + const mono_fields_for_defaults: []const Monotype.FieldKey = if (mono_resolved.kind == .record) + self.store.monotype_store.recordFields(mono_resolved) + else if (mono_resolved.isUnit()) + &.{} + else + typeBindingInvariant( + "lowerPattern(record_destructure): expected record monotype, found kind={d}", + .{@intFromEnum(mono_resolved.kind)}, + ); for (mono_fields_for_defaults) |mono_field| { try self.scratch_pattern_ids.append( - try self.store.addPattern(self.allocator, .wildcard, mono_field.type_idx), + try self.store.addPattern(self.allocator, .wildcard, mono_field.ty), ); } @@ -3196,7 +3006,7 @@ fn lowerPattern(self: *Self, module_env: *const ModuleEnv, pattern_idx: CIR.Patt const destruct = module_env.store.getRecordDestruct(destruct_idx); const pat_idx = destruct.kind.toPatternIdx(); const mir_pat = try self.lowerPattern(module_env, pat_idx); - const mono_fields = self.store.monotype_store.getFields(mono_field_span); + const mono_fields = self.store.monotype_store.recordFields(mono_resolved); const field_idx = self.recordFieldIndexByName(destruct.label, mono_fields); self.scratch_pattern_ids.items.items[@intCast(pats_top + field_idx)] = mir_pat; } @@ -3362,10 +3172,7 @@ fn lowerClosure(self: *Self, module_env: *const ModuleEnv, closure: CIR.Expr.Clo try capture_lookup_exprs_snapshot.appendSlice(self.allocator, capture_lookup_exprs); // --- Step 2: Create captures tuple monotype --- - const captures_tuple_elems = try self.store.monotype_store.addIdxSpan(self.allocator, capture_monotypes_snapshot.items); - const captures_tuple_monotype = try self.store.monotype_store.addMonotype(self.allocator, .{ .tuple = .{ - .elems = captures_tuple_elems, - } }); + const captures_tuple_monotype = try self.store.monotype_store.internTuple(capture_monotypes_snapshot.items); // --- Step 3: Create the lifted function symbol --- // Use closure's tag_name as the basis if available, else synthesize @@ -3447,9 +3254,8 @@ fn lowerClosure(self: *Self, module_env: *const ModuleEnv, closure: CIR.Expr.Clo const all_params = try self.store.addPatternSpan(self.allocator, self.scratch_pattern_ids.sliceFromStart(pat_top)); // Build the lifted function's monotype: func(orig_args..., captures_tuple) -> ret - const orig_monotype = self.store.monotype_store.getMonotype(monotype); - const orig_func = orig_monotype.func; - const orig_arg_monos = self.store.monotype_store.getIdxSpan(orig_func.args); + const resolved_mono = self.store.monotype_store.resolve(monotype); + const orig_arg_monos = self.store.monotype_store.funcArgs(resolved_mono); const arg_top = self.mono_scratches.idxs.top(); defer self.mono_scratches.idxs.clearFrom(arg_top); for (orig_arg_monos) |am| { @@ -3458,8 +3264,8 @@ fn lowerClosure(self: *Self, module_env: *const ModuleEnv, closure: CIR.Expr.Clo try self.mono_scratches.idxs.append(captures_tuple_monotype); const lifted_func_monotype = try self.buildFuncMonotype( self.mono_scratches.idxs.sliceFromStart(arg_top), - orig_func.ret, - orig_func.effectful, + self.store.monotype_store.funcRet(resolved_mono), + self.store.monotype_store.funcEffectful(resolved_mono), ); // Create the lifted lambda expression @@ -3624,11 +3430,12 @@ fn lowerCallWithLoweredFunc( ) Allocator.Error!MIR.ExprId { const lowered_func = try self.stabilizeEscapingFunctionExpr(lowered_func_input); const func_mono = self.store.typeOf(lowered_func); - const call_result_monotype = switch (self.store.monotype_store.getMonotype(func_mono)) { - .func => |f| f.ret, - else => monotype, - }; - if (self.store.monotype_store.getMonotype(func_mono) == .func and self.store.getExpr(lowered_func) != .lookup) { + const func_mono_resolved = self.store.monotype_store.resolve(func_mono); + const call_result_monotype = if (func_mono_resolved.kind == .func) + self.store.monotype_store.funcRet(func_mono_resolved) + else + monotype; + if (func_mono_resolved.kind == .func and self.store.getExpr(lowered_func) != .lookup) { const func_bind = try self.makeSyntheticBind(func_mono, false); try self.registerBoundSymbolDefIfNeeded(func_bind.pattern, lowered_func); const func_lookup = try self.emitMirLookup(func_bind.symbol, func_mono, region); @@ -3960,7 +3767,7 @@ fn lowerBinop(self: *Self, expr_idx: CIR.Expr.Idx, binop: CIR.Expr.Binop, monoty .@"and" => { const cond = try self.lowerExpr(binop.lhs); const body_true = try self.lowerExpr(binop.rhs); - const bool_monotype = try self.store.monotype_store.addBoolTagUnion(self.allocator, self.currentCommonIdents()); + const bool_monotype = try self.store.monotype_store.addBoolTagUnion(); const false_expr = try self.store.addExpr(self.allocator, .{ .tag = .{ .name = module_env.idents.false_tag, .args = MIR.ExprSpan.empty(), @@ -3972,7 +3779,7 @@ fn lowerBinop(self: *Self, expr_idx: CIR.Expr.Idx, binop: CIR.Expr.Binop, monoty .@"or" => { const cond = try self.lowerExpr(binop.lhs); const body_else = try self.lowerExpr(binop.rhs); - const bool_monotype = try self.store.monotype_store.addBoolTagUnion(self.allocator, self.currentCommonIdents()); + const bool_monotype = try self.store.monotype_store.addBoolTagUnion(); const true_expr = try self.store.addExpr(self.allocator, .{ .tag = .{ .name = module_env.idents.true_tag, .args = MIR.ExprSpan.empty(), @@ -3993,8 +3800,8 @@ fn lowerBinop(self: *Self, expr_idx: CIR.Expr.Idx, binop: CIR.Expr.Binop, monoty // Equality on structural types is decomposed field-by-field in MIR // rather than dispatched to a nominal method. if (binop.op == .eq or binop.op == .ne) { - const lhs_mono = self.store.monotype_store.getMonotype(lhs_monotype); - switch (lhs_mono) { + const lhs_resolved = self.store.monotype_store.resolve(lhs_monotype); + switch (lhs_resolved.kind) { // Records, tuples, and lists are always structural. .record, .tuple, .list => { const result = try self.lowerStructuralEquality(lhs, rhs, lhs_monotype, monotype, region); @@ -4014,21 +3821,22 @@ fn lowerBinop(self: *Self, expr_idx: CIR.Expr.Idx, binop: CIR.Expr.Binop, monoty } // Nominal tag union — fall through to method call dispatch below. }, - // Unit is always equal. - .unit => return try self.emitMirBoolLiteral(module_env, binop.op == .eq, region), - // Primitives: emit low-level eq op directly. - .prim => |p| { - const op: CIR.Expr.LowLevel = switch (p) { - .str => .str_is_eq, - else => .num_is_eq, - }; - const args = try self.store.addExprSpan(self.allocator, &.{ lhs, rhs }); - const result = try self.store.addExpr(self.allocator, .{ .run_low_level = .{ - .op = op, - .args = args, - } }, monotype, region); - if (binop.op == .ne) return try self.negBool(module_env, result, monotype, region); - return result; + // Builtins: unit is always equal, prims use low-level eq. + .builtin => { + if (lhs_resolved.isUnit()) return try self.emitMirBoolLiteral(module_env, binop.op == .eq, region); + if (lhs_resolved.builtinPrim()) |p| { + const op: CIR.Expr.LowLevel = switch (p) { + .str => .str_is_eq, + else => .num_is_eq, + }; + const args = try self.store.addExprSpan(self.allocator, &.{ lhs, rhs }); + const result = try self.store.addExpr(self.allocator, .{ .run_low_level = .{ + .op = op, + .args = args, + } }, monotype, region); + if (binop.op == .ne) return try self.negBool(module_env, result, monotype, region); + return result; + } }, else => {}, } @@ -4115,7 +3923,7 @@ fn createBoolMatch( monotype: Monotype.Idx, region: Region, ) Allocator.Error!MIR.ExprId { - const bool_monotype = try self.store.monotype_store.addBoolTagUnion(self.allocator, self.currentCommonIdents()); + const bool_monotype = try self.store.monotype_store.addBoolTagUnion(); const true_pattern = try self.store.addPattern(self.allocator, .{ .tag = .{ .name = module_env.idents.true_tag, @@ -4139,7 +3947,7 @@ fn createBoolMatch( /// Negate a Bool: `match expr { True => False, _ => True }` fn negBool(self: *Self, module_env: *const ModuleEnv, expr: MIR.ExprId, monotype: Monotype.Idx, region: Region) Allocator.Error!MIR.ExprId { - const bool_monotype = try self.store.monotype_store.addBoolTagUnion(self.allocator, self.currentCommonIdents()); + const bool_monotype = try self.store.monotype_store.addBoolTagUnion(); const false_expr = try self.store.addExpr(self.allocator, .{ .tag = .{ .name = module_env.idents.false_tag, .args = MIR.ExprSpan.empty(), @@ -4168,9 +3976,14 @@ fn lowerStructuralEquality( region: Region, ) Allocator.Error!MIR.ExprId { const module_env = self.all_module_envs[self.current_module_idx]; - const mono = self.store.monotype_store.getMonotype(operand_monotype); - return switch (mono) { - .unit => try self.emitMirBoolLiteral(module_env, true, region), + const resolved = self.store.monotype_store.resolve(operand_monotype); + return switch (resolved.kind) { + .builtin => if (resolved.isUnit()) try self.emitMirBoolLiteral(module_env, true, region) else { + if (std.debug.runtime_safety) { + std.debug.panic("lowerStructuralEquality: unexpected builtin monotype", .{}); + } + unreachable; + }, .record, .tuple, .tag_union, .list => blk: { // Structural equality may inspect the same operand many times; bind // each side once so downstream helpers only work with stable lookups. @@ -4179,11 +3992,11 @@ fn lowerStructuralEquality( const lhs_lookup = try self.emitMirLookup(lhs_bind.symbol, operand_monotype, region); const rhs_lookup = try self.emitMirLookup(rhs_bind.symbol, operand_monotype, region); - const inner = switch (mono) { - .record => |rec| try self.lowerRecordEquality(module_env, lhs_lookup, rhs_lookup, rec, ret_monotype, region), - .tuple => |tup| try self.lowerTupleEquality(module_env, lhs_lookup, rhs_lookup, tup, ret_monotype, region), - .tag_union => |tu| try self.lowerTagUnionEquality(module_env, lhs_lookup, rhs_lookup, tu, operand_monotype, ret_monotype, region), - .list => |lst| try self.lowerListEquality(module_env, lhs_lookup, rhs_lookup, lst, ret_monotype, region), + const inner = switch (resolved.kind) { + .record => try self.lowerRecordEquality(module_env, lhs_lookup, rhs_lookup, resolved, ret_monotype, region), + .tuple => try self.lowerTupleEquality(module_env, lhs_lookup, rhs_lookup, resolved, ret_monotype, region), + .tag_union => try self.lowerTagUnionEquality(module_env, lhs_lookup, rhs_lookup, resolved, operand_monotype, ret_monotype, region), + .list => try self.lowerListEquality(module_env, lhs_lookup, rhs_lookup, resolved, ret_monotype, region), else => unreachable, }; @@ -4199,7 +4012,7 @@ fn lowerStructuralEquality( }, else => { if (std.debug.runtime_safety) { - std.debug.panic("lowerStructuralEquality: unexpected monotype {s}", .{@tagName(mono)}); + std.debug.panic("lowerStructuralEquality: unexpected monotype kind={d}", .{@intFromEnum(resolved.kind)}); } unreachable; }, @@ -4218,28 +4031,34 @@ fn lowerFieldEquality( ret_monotype: Monotype.Idx, region: Region, ) Allocator.Error!MIR.ExprId { - const mono = self.store.monotype_store.getMonotype(field_monotype); - switch (mono) { - .prim => |p| { - const op: CIR.Expr.LowLevel = switch (p) { - .str => .str_is_eq, - else => .num_is_eq, - }; - const args = try self.store.addExprSpan(self.allocator, &.{ lhs, rhs }); - return try self.store.addExpr(self.allocator, .{ .run_low_level = .{ - .op = op, - .args = args, - } }, ret_monotype, region); + const resolved = self.store.monotype_store.resolve(field_monotype); + switch (resolved.kind) { + .builtin => { + if (resolved.isUnit()) { + return try self.emitMirBoolLiteral(module_env, true, region); + } + if (resolved.builtinPrim()) |p| { + const op: CIR.Expr.LowLevel = switch (p) { + .str => .str_is_eq, + else => .num_is_eq, + }; + const args = try self.store.addExprSpan(self.allocator, &.{ lhs, rhs }); + return try self.store.addExpr(self.allocator, .{ .run_low_level = .{ + .op = op, + .args = args, + } }, ret_monotype, region); + } + if (std.debug.runtime_safety) { + std.debug.panic("lowerFieldEquality: unexpected builtin monotype", .{}); + } + unreachable; }, .record, .tuple, .tag_union, .list => { return try self.lowerStructuralEquality(lhs, rhs, field_monotype, ret_monotype, region); }, - .unit => { - return try self.emitMirBoolLiteral(module_env, true, region); - }, else => { if (std.debug.runtime_safety) { - std.debug.panic("lowerFieldEquality: unexpected field monotype {s}", .{@tagName(mono)}); + std.debug.panic("lowerFieldEquality: unexpected field monotype kind={d}", .{@intFromEnum(resolved.kind)}); } unreachable; }, @@ -4261,25 +4080,24 @@ fn lowerRecordEquality( self.debugAssertLookupExpr(lhs, "lowerRecordEquality(lhs)"); self.debugAssertLookupExpr(rhs, "lowerRecordEquality(rhs)"); - const field_span = rec.fields; - const fields = self.store.monotype_store.getFields(field_span); + const fields = self.store.monotype_store.recordFields(rec); // Empty record: always equal if (fields.len == 0) return try self.emitMirBoolLiteral(module_env, true, region); // Build field comparisons from last to first (innermost result first). var result: MIR.ExprId = undefined; - var i: usize = field_span.len; + var i: usize = fields.len; while (i > 0) { i -= 1; - const field = self.store.monotype_store.getFields(field_span)[i]; + const field = self.store.monotype_store.recordFields(rec)[i]; - const lhs_field = try self.emitMirStructAccess(lhs, @intCast(i), field.type_idx, region); - const rhs_field = try self.emitMirStructAccess(rhs, @intCast(i), field.type_idx, region); + const lhs_field = try self.emitMirStructAccess(lhs, @intCast(i), field.ty, region); + const rhs_field = try self.emitMirStructAccess(rhs, @intCast(i), field.ty, region); - const field_eq = try self.lowerFieldEquality(module_env, lhs_field, rhs_field, field.type_idx, ret_monotype, region); + const field_eq = try self.lowerFieldEquality(module_env, lhs_field, rhs_field, field.ty, ret_monotype, region); - if (i == field_span.len - 1) { + if (i == fields.len - 1) { result = field_eq; } else { // Short-circuit AND: match field_eq { True => , _ => False } @@ -4304,25 +4122,24 @@ fn lowerTupleEquality( self.debugAssertLookupExpr(lhs, "lowerTupleEquality(lhs)"); self.debugAssertLookupExpr(rhs, "lowerTupleEquality(rhs)"); - const elem_span = tup.elems; - const elems = self.store.monotype_store.getIdxSpan(elem_span); + const elems = self.store.monotype_store.tupleElems(tup); // Empty tuple: always equal if (elems.len == 0) return try self.emitMirBoolLiteral(module_env, true, region); // Build element comparisons from last to first. var result: MIR.ExprId = undefined; - var i: usize = elem_span.len; + var i: usize = elems.len; while (i > 0) { i -= 1; - const elem_mono = self.store.monotype_store.getIdxSpan(elem_span)[i]; + const elem_mono = self.store.monotype_store.tupleElems(tup)[i]; const lhs_elem = try self.emitMirStructAccess(lhs, @intCast(i), elem_mono, region); const rhs_elem = try self.emitMirStructAccess(rhs, @intCast(i), elem_mono, region); const elem_eq = try self.lowerFieldEquality(module_env, lhs_elem, rhs_elem, elem_mono, ret_monotype, region); - if (i == elem_span.len - 1) { + if (i == elems.len - 1) { result = elem_eq; } else { const false_expr = try self.emitMirBoolLiteral(module_env, false, region); @@ -4349,8 +4166,7 @@ fn lowerTagUnionEquality( self.debugAssertLookupExpr(lhs, "lowerTagUnionEquality(lhs)"); self.debugAssertLookupExpr(rhs, "lowerTagUnionEquality(rhs)"); - const tag_span = tu.tags; - const tags = self.store.monotype_store.getTags(tag_span); + const tags = self.store.monotype_store.tagUnionTags(tu); // Empty tag union: vacuously true if (tags.len == 0) return try self.emitMirBoolLiteral(module_env, true, region); @@ -4358,17 +4174,17 @@ fn lowerTagUnionEquality( const save_branches = self.scratch_branches.top(); defer self.scratch_branches.clearFrom(save_branches); - for (0..tag_span.len) |tag_i| { - const current_tags = self.store.monotype_store.getTags(tag_span); + for (0..tags.len) |tag_i| { + const current_tags = self.store.monotype_store.tagUnionTags(tu); const tag = current_tags[tag_i]; - const payload_span = tag.payloads; + const payloads = self.store.monotype_store.getIdxListItems(tag.payloads); // --- LHS pattern: Tag(lhs_p0, lhs_p1, ...) --- const save_pats = self.scratch_pattern_ids.top(); const save_caps = self.scratch_captures.top(); defer self.scratch_captures.clearFrom(save_caps); - for (self.store.monotype_store.getIdxSpan(payload_span)) |payload_mono| { + for (payloads) |payload_mono| { const bind = try self.makeSyntheticBind(payload_mono, false); try self.scratch_pattern_ids.append(bind.pattern); try self.scratch_captures.append(.{ .symbol = bind.symbol }); @@ -4378,10 +4194,11 @@ fn lowerTagUnionEquality( self.scratch_pattern_ids.sliceFromStart(save_pats), ); self.scratch_pattern_ids.clearFrom(save_pats); - const lhs_tag_args = try self.wrapMultiPayloadTagPatterns(tag.name, tu_monotype, lhs_payload_patterns); + const tag_ident = self.identFromStructuralName(tag.name); + const lhs_tag_args = try self.wrapMultiPayloadTagPatterns(tag_ident, tu_monotype, lhs_payload_patterns); const lhs_tag_pattern = try self.store.addPattern(self.allocator, .{ .tag = .{ - .name = tag.name, + .name = tag_ident, .args = lhs_tag_args, } }, tu_monotype); const lhs_bp = try self.store.addBranchPatterns(self.allocator, &.{.{ @@ -4390,7 +4207,7 @@ fn lowerTagUnionEquality( }}); // --- RHS pattern: Tag(rhs_p0, rhs_p1, ...) --- - for (self.store.monotype_store.getIdxSpan(payload_span)) |payload_mono| { + for (payloads) |payload_mono| { const bind = try self.makeSyntheticBind(payload_mono, false); try self.scratch_pattern_ids.append(bind.pattern); try self.scratch_captures.append(.{ .symbol = bind.symbol }); @@ -4400,10 +4217,10 @@ fn lowerTagUnionEquality( self.scratch_pattern_ids.sliceFromStart(save_pats), ); self.scratch_pattern_ids.clearFrom(save_pats); - const rhs_tag_args = try self.wrapMultiPayloadTagPatterns(tag.name, tu_monotype, rhs_payload_patterns); + const rhs_tag_args = try self.wrapMultiPayloadTagPatterns(tag_ident, tu_monotype, rhs_payload_patterns); const rhs_tag_pattern = try self.store.addPattern(self.allocator, .{ .tag = .{ - .name = tag.name, + .name = tag_ident, .args = rhs_tag_args, } }, tu_monotype); const rhs_bp = try self.store.addBranchPatterns(self.allocator, &.{.{ @@ -4413,23 +4230,23 @@ fn lowerTagUnionEquality( // --- Build payload comparison --- const all_caps = self.scratch_captures.sliceFromStart(save_caps); - const payload_eq = if (payload_span.len == 0) + const payload_eq = if (payloads.len == 0) try self.emitMirBoolLiteral(module_env, true, region) else blk: { - const lhs_caps = all_caps[0..payload_span.len]; - const rhs_caps = all_caps[payload_span.len..][0..payload_span.len]; + const lhs_caps = all_caps[0..payloads.len]; + const rhs_caps = all_caps[payloads.len..][0..payloads.len]; // Chain payload comparisons with short-circuit AND (last to first) var payload_result: MIR.ExprId = undefined; - var j: usize = payload_span.len; + var j: usize = payloads.len; while (j > 0) { j -= 1; - const payload_mono = self.store.monotype_store.getIdxSpan(payload_span)[j]; + const payload_mono = self.store.monotype_store.getIdxListItems(tag.payloads)[j]; const lhs_lookup = try self.emitMirLookup(lhs_caps[j].symbol, payload_mono, region); const rhs_lookup = try self.emitMirLookup(rhs_caps[j].symbol, payload_mono, region); const field_eq = try self.lowerFieldEquality(module_env, lhs_lookup, rhs_lookup, payload_mono, ret_monotype, region); - if (j == payload_span.len - 1) { + if (j == payloads.len - 1) { payload_result = field_eq; } else { const false_expr = try self.emitMirBoolLiteral(module_env, false, region); @@ -4503,7 +4320,7 @@ fn lowerListEquality( self.debugAssertLookupExpr(rhs, "lowerListEquality(rhs)"); const u64_mono = self.store.monotype_store.primIdx(.u64); - const bool_mono = try self.store.monotype_store.addBoolTagUnion(self.allocator, self.currentCommonIdents()); + const bool_mono = try self.store.monotype_store.addBoolTagUnion(); const unit_mono = self.store.monotype_store.unit_idx; // len = list_len(lhs) @@ -4559,17 +4376,17 @@ fn lowerListEquality( const lhs_elem = try self.store.addExpr(self.allocator, .{ .run_low_level = .{ .op = .list_get_unsafe, .args = lhs_get_args, - } }, lst.elem, region); + } }, self.store.monotype_store.listElem(lst), region); const i_lookup_rhs = try self.emitMirLookup(i_bind.symbol, u64_mono, region); const rhs_get_args = try self.store.addExprSpan(self.allocator, &.{ rhs, i_lookup_rhs }); const rhs_elem = try self.store.addExpr(self.allocator, .{ .run_low_level = .{ .op = .list_get_unsafe, .args = rhs_get_args, - } }, lst.elem, region); + } }, self.store.monotype_store.listElem(lst), region); // Compare elements - const elem_eq = try self.lowerFieldEquality(module_env, lhs_elem, rhs_elem, lst.elem, ret_monotype, region); + const elem_eq = try self.lowerFieldEquality(module_env, lhs_elem, rhs_elem, self.store.monotype_store.listElem(lst), ret_monotype, region); // i = num_plus(i, 1) const i_lookup_inc = try self.emitMirLookup(i_bind.symbol, u64_mono, region); @@ -4648,10 +4465,11 @@ fn lowerDotAccess(self: *Self, module_env: *const ModuleEnv, expr_idx: CIR.Expr. const receiver = try self.lowerExpr(da.receiver); const rcv_mono_idx = try self.resolveMonotype(da.receiver); if (rcv_mono_idx.isNone()) break :structural_eq; - const rcv_mono = self.store.monotype_store.getMonotype(rcv_mono_idx); - switch (rcv_mono) { - // Records, tuples, lists, and unit are always structural. - .record, .tuple, .list, .unit => {}, + const rcv_resolved = self.store.monotype_store.resolve(rcv_mono_idx); + switch (rcv_resolved.kind) { + // Records, tuples, lists are always structural. Unit (builtin) too. + .record, .tuple, .list => {}, + .builtin => if (!rcv_resolved.isUnit()) break :structural_eq, // Tag unions may be nominal or anonymous structural. .tag_union => if (self.lookupResolvedDispatchTarget(expr_idx) != null) break :structural_eq, else => break :structural_eq, @@ -4758,17 +4576,14 @@ fn lowerDotAccess(self: *Self, module_env: *const ModuleEnv, expr_idx: CIR.Expr. &self.nominal_cycle_breakers, ); if (!dispatch_func_monotype.isNone()) { - const dispatch_mono = self.store.monotype_store.getMonotype(dispatch_func_monotype); - const dispatch_func = switch (dispatch_mono) { - .func => |f| f, - else => { - typeBindingInvariant( - "lowerDotAccess: dispatch fn_var monotype is not function (method='{s}', monotype='{s}')", - .{ module_env.getIdent(da.field_name), @tagName(dispatch_mono) }, - ); - }, - }; - const dispatch_args = self.store.monotype_store.getIdxSpan(dispatch_func.args); + const dispatch_resolved = self.store.monotype_store.resolve(dispatch_func_monotype); + if (dispatch_resolved.kind != .func) { + typeBindingInvariant( + "lowerDotAccess: dispatch fn_var monotype is not function (method='{s}', kind={d})", + .{ module_env.getIdent(da.field_name), @intFromEnum(dispatch_resolved.kind) }, + ); + } + const dispatch_args = self.store.monotype_store.funcArgs(dispatch_resolved); if (dispatch_args.len > 0) { expected_param_monos = dispatch_args; } @@ -4806,11 +4621,10 @@ fn lowerDotAccess(self: *Self, module_env: *const ModuleEnv, expr_idx: CIR.Expr. &self.nominal_cycle_breakers, ); if (!refined.isNone()) { - const refined_mono = self.store.monotype_store.getMonotype(refined); - if (refined_mono != .func) { + if (self.store.monotype_store.resolve(refined).kind != .func) { typeBindingInvariant( - "lowerDotAccess: refined dispatch fn_var monotype is not function (method='{s}', monotype='{s}')", - .{ module_env.getIdent(da.field_name), @tagName(refined_mono) }, + "lowerDotAccess: refined dispatch fn_var monotype is not function (method='{s}', kind={d})", + .{ module_env.getIdent(da.field_name), @intFromEnum(self.store.monotype_store.resolve(refined).kind) }, ); } break :blk refined; @@ -4827,10 +4641,11 @@ fn lowerDotAccess(self: *Self, module_env: *const ModuleEnv, expr_idx: CIR.Expr. method_effectful, ); }; - const call_result_monotype = switch (self.store.monotype_store.getMonotype(method_func_monotype)) { - .func => |f| f.ret, - else => monotype, - }; + const mfm_resolved = self.store.monotype_store.resolve(method_func_monotype); + const call_result_monotype = if (mfm_resolved.kind == .func) + self.store.monotype_store.funcRet(mfm_resolved) + else + monotype; // Ensure the method body is lowered so codegen can find it. const lowered_method_symbol = try self.specializeMethod(method_symbol, method_func_monotype); @@ -4846,16 +4661,16 @@ fn lowerDotAccess(self: *Self, module_env: *const ModuleEnv, expr_idx: CIR.Expr. // Field access const receiver = try self.lowerExpr(da.receiver); const receiver_monotype = self.store.typeOf(receiver); - const receiver_record = switch (self.store.monotype_store.getMonotype(receiver_monotype)) { - .record => |record| record, - else => typeBindingInvariant( - "lowerDotAccess: field access receiver is not a record monotype (field='{s}', monotype='{s}')", - .{ module_env.getIdent(da.field_name), @tagName(self.store.monotype_store.getMonotype(receiver_monotype)) }, - ), - }; + const receiver_resolved = self.store.monotype_store.resolve(receiver_monotype); + if (receiver_resolved.kind != .record) { + typeBindingInvariant( + "lowerDotAccess: field access receiver is not a record monotype (field='{s}', kind={d})", + .{ module_env.getIdent(da.field_name), @intFromEnum(receiver_resolved.kind) }, + ); + } const field_idx = self.recordFieldIndexByName( da.field_name, - self.store.monotype_store.getFields(receiver_record.fields), + self.store.monotype_store.recordFields(receiver_resolved), ); return try self.emitMirStructAccess(receiver, field_idx, monotype, region); } @@ -4864,14 +4679,10 @@ fn lowerDotAccess(self: *Self, module_env: *const ModuleEnv, expr_idx: CIR.Expr. /// Lower a CIR record expression. fn lowerRecord(self: *Self, module_env: *const ModuleEnv, record: anytype, monotype: Monotype.Idx, region: Region) Allocator.Error!MIR.ExprId { const cir_field_indices = module_env.store.sliceRecordFields(record.fields); - const mono_record = switch (self.store.monotype_store.getMonotype(monotype)) { - .record => |mono_record| mono_record, - else => typeBindingInvariant( - "lowerRecord: expected record monotype, found '{s}'", - .{@tagName(self.store.monotype_store.getMonotype(monotype))}, - ), - }; - const mono_field_span = mono_record.fields; + const record_resolved = self.store.monotype_store.resolve(monotype); + if (record_resolved.kind != .record) { + typeBindingInvariant("lowerRecord: expected record monotype", .{}); + } const ProvidedField = struct { name: Ident.Idx, @@ -4899,11 +4710,12 @@ fn lowerRecord(self: *Self, module_env: *const ModuleEnv, record: anytype, monot const exprs_top = self.scratch_expr_ids.top(); defer self.scratch_expr_ids.clearFrom(exprs_top); - const canonical_field_exprs = try self.allocator.alloc(?MIR.ExprId, mono_field_span.len); + const record_mono_fields = self.store.monotype_store.recordFields(record_resolved); + const canonical_field_exprs = try self.allocator.alloc(?MIR.ExprId, record_mono_fields.len); defer self.allocator.free(canonical_field_exprs); @memset(canonical_field_exprs, null); - const mono_fields = self.store.monotype_store.getFields(mono_field_span); + const mono_fields = record_mono_fields; for (provided_fields.items) |provided| { const field_idx = self.recordFieldIndexByName(provided.name, mono_fields); canonical_field_exprs[@intCast(field_idx)] = provided.expr; @@ -4929,12 +4741,12 @@ fn lowerRecord(self: *Self, module_env: *const ModuleEnv, record: anytype, monot // Record update: include all fields in the resulting record. // Updated fields use explicit expressions; missing fields become accesses on the // base record expression from `..record`. - const updated_mono_fields = self.store.monotype_store.getFields(mono_field_span); + const updated_mono_fields = self.store.monotype_store.recordFields(record_resolved); for (updated_mono_fields, 0..) |mono_field, field_idx| { const field_expr = canonical_field_exprs[field_idx] orelse try self.emitMirStructAccess( extension_binding.?.lookup, @intCast(field_idx), - mono_field.type_idx, + mono_field.ty, region, ); @@ -5199,9 +5011,9 @@ fn resolveDispatchTargetForDotCall( continue; } - const mono = self.store.monotype_store.getMonotype(fn_mono); - if (mono != .func) continue; - const fn_args = self.store.monotype_store.getIdxSpan(mono.func.args); + const fn_resolved = self.store.monotype_store.resolve(fn_mono); + if (fn_resolved.kind != .func) continue; + const fn_args = self.store.monotype_store.funcArgs(fn_resolved); const compatible = if (fn_args.len == 0) true else @@ -5737,45 +5549,49 @@ fn bindRecordFieldByName( self: *Self, field_name: Ident.Idx, field_var: types.Var, - mono_fields: []const Monotype.Field, + mono_fields: []const Monotype.FieldKey, ) Allocator.Error!void { const field_idx = self.recordFieldIndexByName(field_name, mono_fields); - try self.bindTypeVarMonotypes(field_var, mono_fields[@intCast(field_idx)].type_idx); + try self.bindTypeVarMonotypes(field_var, mono_fields[@intCast(field_idx)].ty); } fn recordFieldIndexByName( self: *Self, field_name: Ident.Idx, - mono_fields: []const Monotype.Field, + mono_fields: []const Monotype.FieldKey, ) u32 { + const field_name_text = self.all_module_envs[self.current_module_idx].getIdent(field_name); for (mono_fields, 0..) |mono_field, field_idx| { - if (self.identsStructurallyEqual(mono_field.name, field_name)) { + if (std.mem.eql(u8, self.store.monotype_store.getNameText(mono_field.name), field_name_text)) { return @intCast(field_idx); } } - const module_env = self.all_module_envs[self.current_module_idx]; typeBindingInvariant( "record field '{s}' missing from monotype", - .{module_env.getIdent(field_name)}, + .{field_name_text}, ); } fn tagIndexByName( self: *Self, tag_name: Ident.Idx, - mono_tags: []const Monotype.Tag, + mono_tags: []const Monotype.TagKey, ) u32 { + const tag_name_text = self.all_module_envs[self.current_module_idx].getIdent(tag_name); + const tag_last_segment = identLastSegment(tag_name_text); for (mono_tags, 0..) |mono_tag, tag_idx| { - if (self.identsTagNameEquivalent(mono_tag.name, tag_name)) { + const mono_tag_text = self.store.monotype_store.getNameText(mono_tag.name); + if (std.mem.eql(u8, mono_tag_text, tag_name_text) or + std.mem.eql(u8, identLastSegment(mono_tag_text), tag_last_segment)) + { return @intCast(tag_idx); } } - const module_env = self.all_module_envs[self.current_module_idx]; typeBindingInvariant( "tag '{s}' missing from monotype", - .{module_env.getIdent(tag_name)}, + .{tag_name_text}, ); } @@ -5797,10 +5613,10 @@ fn appendSeenIndex( fn remainingRecordTailMonotype( self: *Self, - mono_fields: []const Monotype.Field, + mono_fields: []const Monotype.FieldKey, seen_indices: []const u32, ) Allocator.Error!Monotype.Idx { - var remaining_fields: std.ArrayListUnmanaged(Monotype.Field) = .empty; + var remaining_fields: std.ArrayListUnmanaged(Monotype.FieldKey) = .empty; defer remaining_fields.deinit(self.allocator); for (mono_fields, 0..) |field, field_idx| { @@ -5812,16 +5628,15 @@ fn remainingRecordTailMonotype( return self.store.monotype_store.unit_idx; } - const field_span = try self.store.monotype_store.addFields(self.allocator, remaining_fields.items); - return try self.store.monotype_store.addMonotype(self.allocator, .{ .record = .{ .fields = field_span } }); + return try self.store.monotype_store.internRecord(remaining_fields.items); } fn remainingTagUnionTailMonotype( self: *Self, - mono_tags: []const Monotype.Tag, + mono_tags: []const Monotype.TagKey, seen_indices: []const u32, ) Allocator.Error!Monotype.Idx { - var remaining_tags: std.ArrayListUnmanaged(Monotype.Tag) = .empty; + var remaining_tags: std.ArrayListUnmanaged(Monotype.TagKey) = .empty; defer remaining_tags.deinit(self.allocator); for (mono_tags, 0..) |tag, tag_idx| { @@ -5829,8 +5644,7 @@ fn remainingTagUnionTailMonotype( try remaining_tags.append(self.allocator, tag); } - const tag_span = try self.store.monotype_store.addTags(self.allocator, remaining_tags.items); - return try self.store.monotype_store.addMonotype(self.allocator, .{ .tag_union = .{ .tags = tag_span } }); + return try self.store.monotype_store.internTagUnion(remaining_tags.items); } fn bindRecordRowTailInStore( @@ -5839,7 +5653,7 @@ fn bindRecordRowTailInStore( common_idents: ModuleEnv.CommonIdents, bindings: *std.AutoHashMap(types.Var, Monotype.Idx), ext_var: types.Var, - mono_fields: []const Monotype.Field, + mono_fields: []const Monotype.FieldKey, seen_indices: []const u32, ) Allocator.Error!void { const tail_mono = try self.remainingRecordTailMonotype(mono_fields, seen_indices); @@ -5849,7 +5663,7 @@ fn bindRecordRowTailInStore( fn bindRecordRowTail( self: *Self, ext_var: types.Var, - mono_fields: []const Monotype.Field, + mono_fields: []const Monotype.FieldKey, seen_indices: []const u32, ) Allocator.Error!void { const tail_mono = try self.remainingRecordTailMonotype(mono_fields, seen_indices); @@ -5862,7 +5676,7 @@ fn bindTagUnionRowTailInStore( common_idents: ModuleEnv.CommonIdents, bindings: *std.AutoHashMap(types.Var, Monotype.Idx), ext_var: types.Var, - mono_tags: []const Monotype.Tag, + mono_tags: []const Monotype.TagKey, seen_indices: []const u32, ) Allocator.Error!void { const tail_mono = try self.remainingTagUnionTailMonotype(mono_tags, seen_indices); @@ -5872,7 +5686,7 @@ fn bindTagUnionRowTailInStore( fn bindTagUnionRowTail( self: *Self, ext_var: types.Var, - mono_tags: []const Monotype.Tag, + mono_tags: []const Monotype.TagKey, seen_indices: []const u32, ) Allocator.Error!void { const tail_mono = try self.remainingTagUnionTailMonotype(mono_tags, seen_indices); @@ -5880,8 +5694,7 @@ fn bindTagUnionRowTail( } fn tupleMonotypeForFields(self: *Self, field_monotypes: []const Monotype.Idx) Allocator.Error!Monotype.Idx { - const elems = try self.store.monotype_store.addIdxSpan(self.allocator, field_monotypes); - return try self.store.monotype_store.addMonotype(self.allocator, .{ .tuple = .{ .elems = elems } }); + return try self.store.monotype_store.internTuple(field_monotypes); } fn tagPayloadMonotypesByName( @@ -5889,25 +5702,29 @@ fn tagPayloadMonotypesByName( union_monotype: Monotype.Idx, tag_name: Ident.Idx, ) []const Monotype.Idx { - const mono = self.store.monotype_store.getMonotype(union_monotype); - const tags = switch (mono) { - .tag_union => |tu| self.store.monotype_store.getTags(tu.tags), - else => typeBindingInvariant( - "tag payload lookup expected tag_union monotype, found '{s}'", - .{@tagName(mono)}, - ), - }; + const resolved = self.store.monotype_store.resolve(union_monotype); + if (resolved.kind != .tag_union) { + typeBindingInvariant( + "tag payload lookup expected tag_union monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } + const tags = self.store.monotype_store.tagUnionTags(resolved); + const tag_name_text = self.all_module_envs[self.current_module_idx].getIdent(tag_name); + const tag_last_segment = identLastSegment(tag_name_text); for (tags) |tag| { - if (self.identsTagNameEquivalent(tag.name, tag_name)) { - return self.store.monotype_store.getIdxSpan(tag.payloads); + const mono_tag_text = self.store.monotype_store.getNameText(tag.name); + if (std.mem.eql(u8, mono_tag_text, tag_name_text) or + std.mem.eql(u8, identLastSegment(mono_tag_text), tag_last_segment)) + { + return self.store.monotype_store.getIdxListItems(tag.payloads); } } - const module_env = self.all_module_envs[self.current_module_idx]; typeBindingInvariant( "tag '{s}' missing from monotype", - .{module_env.getIdent(tag_name)}, + .{tag_name_text}, ); } @@ -5964,30 +5781,31 @@ fn bindTagPayloadsByName( self: *Self, tag_name: Ident.Idx, payload_vars: []const types.Var, - mono_tags: []const Monotype.Tag, + mono_tags: []const Monotype.TagKey, ) Allocator.Error!void { + const tag_name_text = self.all_module_envs[self.current_module_idx].getIdent(tag_name); + const tag_last_segment = identLastSegment(tag_name_text); for (mono_tags) |mono_tag| { - if (!self.identsTagNameEquivalent(mono_tag.name, tag_name)) continue; + const mono_tag_text = self.store.monotype_store.getNameText(mono_tag.name); + if (!std.mem.eql(u8, mono_tag_text, tag_name_text) and + !std.mem.eql(u8, identLastSegment(mono_tag_text), tag_last_segment)) continue; - const mono_payload_span = mono_tag.payloads; - if (payload_vars.len != mono_payload_span.len) { - const module_env = self.all_module_envs[self.current_module_idx]; + const mono_payloads = self.store.monotype_store.getIdxListItems(mono_tag.payloads); + if (payload_vars.len != mono_payloads.len) { typeBindingInvariant( "bindFlatTypeMonotypes(tag_union): payload arity mismatch for tag '{s}'", - .{module_env.getIdent(tag_name)}, + .{tag_name_text}, ); } for (payload_vars, 0..) |payload_var, i| { - const mono_payload = self.store.monotype_store.getIdxSpan(mono_payload_span)[i]; - try self.bindTypeVarMonotypes(payload_var, mono_payload); + try self.bindTypeVarMonotypes(payload_var, mono_payloads[i]); } return; } - const module_env = self.all_module_envs[self.current_module_idx]; typeBindingInvariant( "bindFlatTypeMonotypes(tag_union): tag '{s}' missing from monotype", - .{module_env.getIdent(tag_name)}, + .{tag_name_text}, ); } @@ -6019,31 +5837,28 @@ fn bindTypeVarMonotypes(self: *Self, type_var: types.Var, monotype: Monotype.Idx fn bindFlatTypeMonotypes(self: *Self, flat_type: types.FlatType, monotype: Monotype.Idx) Allocator.Error!void { if (monotype.isNone()) return; - const mono = self.store.monotype_store.getMonotype(monotype); - const module_env = self.all_module_envs[self.current_module_idx]; + const resolved = self.store.monotype_store.resolve(monotype); switch (flat_type) { .fn_pure, .fn_effectful, .fn_unbound => |func| { - const mfunc = switch (mono) { - .func => |mfunc| mfunc, - else => typeBindingInvariant( - "bindFlatTypeMonotypes(fn): expected function monotype, found '{s}'", - .{@tagName(mono)}, - ), - }; + if (resolved.kind != .func) { + typeBindingInvariant( + "bindFlatTypeMonotypes(fn): expected function monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } const type_args = self.types_store.sliceVars(func.args); - const mono_arg_span = mfunc.args; - if (type_args.len != mono_arg_span.len) { + const mono_args = self.store.monotype_store.funcArgs(resolved); + if (type_args.len != mono_args.len) { typeBindingInvariant( "bindFlatTypeMonotypes(fn): arity mismatch (type={d}, monotype={d})", - .{ type_args.len, mono_arg_span.len }, + .{ type_args.len, mono_args.len }, ); } for (type_args, 0..) |ta, i| { - const ma = self.store.monotype_store.getIdxSpan(mono_arg_span)[i]; - try self.bindTypeVarMonotypes(ta, ma); + try self.bindTypeVarMonotypes(ta, mono_args[i]); } - try self.bindTypeVarMonotypes(func.ret, mfunc.ret); + try self.bindTypeVarMonotypes(func.ret, self.store.monotype_store.funcRet(resolved)); }, .nominal_type => |nominal| { const common = self.currentCommonIdents(); @@ -6051,13 +5866,12 @@ fn bindFlatTypeMonotypes(self: *Self, flat_type: types.FlatType, monotype: Monot const origin = nominal.origin_module; if (origin.eql(common.builtin_module) and ident.eql(common.list)) { - const mlist = switch (mono) { - .list => |mlist| mlist, - else => typeBindingInvariant( - "bindFlatTypeMonotypes(nominal List): expected list monotype, found '{s}'", - .{@tagName(mono)}, - ), - }; + if (resolved.kind != .list) { + typeBindingInvariant( + "bindFlatTypeMonotypes(nominal List): expected list monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } const type_args = self.types_store.sliceNominalArgs(nominal); if (type_args.len != 1) { typeBindingInvariant( @@ -6065,17 +5879,16 @@ fn bindFlatTypeMonotypes(self: *Self, flat_type: types.FlatType, monotype: Monot .{type_args.len}, ); } - try self.bindTypeVarMonotypes(type_args[0], mlist.elem); + try self.bindTypeVarMonotypes(type_args[0], self.store.monotype_store.listElem(resolved)); return; } if (origin.eql(common.builtin_module) and ident.eql(common.box)) { - const mbox = switch (mono) { - .box => |mbox| mbox, - else => typeBindingInvariant( - "bindFlatTypeMonotypes(nominal Box): expected box monotype, found '{s}'", - .{@tagName(mono)}, - ), - }; + if (resolved.kind != .box) { + typeBindingInvariant( + "bindFlatTypeMonotypes(nominal Box): expected box monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } const type_args = self.types_store.sliceNominalArgs(nominal); if (type_args.len != 1) { typeBindingInvariant( @@ -6083,17 +5896,16 @@ fn bindFlatTypeMonotypes(self: *Self, flat_type: types.FlatType, monotype: Monot .{type_args.len}, ); } - try self.bindTypeVarMonotypes(type_args[0], mbox.inner); + try self.bindTypeVarMonotypes(type_args[0], self.store.monotype_store.boxInner(resolved)); return; } if (origin.eql(common.builtin_module) and builtinPrimForNominal(ident, common) != null) { - switch (mono) { - .prim => {}, - else => typeBindingInvariant( - "bindFlatTypeMonotypes(nominal prim): expected prim monotype, found '{s}'", - .{@tagName(mono)}, - ), + if (resolved.kind != .builtin or resolved.builtinPrim() == null) { + typeBindingInvariant( + "bindFlatTypeMonotypes(nominal prim): expected prim monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); } return; } @@ -6103,15 +5915,13 @@ fn bindFlatTypeMonotypes(self: *Self, flat_type: types.FlatType, monotype: Monot try self.bindTypeVarMonotypes(backing_var, monotype); }, .record => |record| { - const mrec = switch (mono) { - .record => |mrec| mrec, - else => typeBindingInvariant( - "bindFlatTypeMonotypes(record): expected record monotype, found '{s}'", - .{@tagName(mono)}, - ), - }; - const mono_field_span = mrec.fields; - const mono_fields = self.store.monotype_store.getFields(mono_field_span); + if (resolved.kind != .record) { + typeBindingInvariant( + "bindFlatTypeMonotypes(record): expected record monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } + const mono_fields = self.store.monotype_store.recordFields(resolved); var seen_field_indices: std.ArrayListUnmanaged(u32) = .empty; defer seen_field_indices.deinit(self.allocator); @@ -6124,7 +5934,7 @@ fn bindFlatTypeMonotypes(self: *Self, flat_type: types.FlatType, monotype: Monot for (field_names, field_vars) |field_name, field_var| { const field_idx = self.recordFieldIndexByName(field_name, mono_fields); try appendSeenIndex(self.allocator, &seen_field_indices, field_idx); - try self.bindTypeVarMonotypes(field_var, mono_fields[field_idx].type_idx); + try self.bindTypeVarMonotypes(field_var, mono_fields[field_idx].ty); } var ext_var = current_row.ext; @@ -6147,7 +5957,7 @@ fn bindFlatTypeMonotypes(self: *Self, flat_type: types.FlatType, monotype: Monot for (ext_field_names, ext_field_vars) |field_name, field_var| { const field_idx = self.recordFieldIndexByName(field_name, mono_fields); try appendSeenIndex(self.allocator, &seen_field_indices, field_idx); - try self.bindTypeVarMonotypes(field_var, mono_fields[field_idx].type_idx); + try self.bindTypeVarMonotypes(field_var, mono_fields[field_idx].ty); } break :rows; }, @@ -6176,65 +5986,61 @@ fn bindFlatTypeMonotypes(self: *Self, flat_type: types.FlatType, monotype: Monot if (!seenIndex(seen_field_indices.items, @intCast(field_idx))) { typeBindingInvariant( "bindFlatTypeMonotypes(record): monotype field '{s}' missing from type row", - .{module_env.getIdent(mono_field.name)}, + .{self.store.monotype_store.getNameText(mono_field.name)}, ); } } }, .record_unbound => |fields_range| { - const mrec = switch (mono) { - .record => |mrec| mrec, - else => typeBindingInvariant( - "bindFlatTypeMonotypes(record_unbound): expected record monotype, found '{s}'", - .{@tagName(mono)}, - ), - }; + if (resolved.kind != .record) { + typeBindingInvariant( + "bindFlatTypeMonotypes(record_unbound): expected record monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } const fields_slice = self.types_store.getRecordFieldsSlice(fields_range); const field_names = fields_slice.items(.name); const field_vars = fields_slice.items(.var_); - const mono_field_span = mrec.fields; + const mono_fields = self.store.monotype_store.recordFields(resolved); - if (field_names.len != mono_field_span.len) { + if (field_names.len != mono_fields.len) { typeBindingInvariant( "bindFlatTypeMonotypes(record_unbound): field count mismatch (type={d}, monotype={d})", - .{ field_names.len, mono_field_span.len }, + .{ field_names.len, mono_fields.len }, ); } for (field_names, field_vars) |field_name, field_var| { - try self.bindRecordFieldByName(field_name, field_var, self.store.monotype_store.getFields(mono_field_span)); + try self.bindRecordFieldByName(field_name, field_var, mono_fields); } }, .tuple => |tuple| { - const mtuple = switch (mono) { - .tuple => |mtuple| mtuple, - else => typeBindingInvariant( - "bindFlatTypeMonotypes(tuple): expected tuple monotype, found '{s}'", - .{@tagName(mono)}, - ), - }; + if (resolved.kind != .tuple) { + typeBindingInvariant( + "bindFlatTypeMonotypes(tuple): expected tuple monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } const elem_vars = self.types_store.sliceVars(tuple.elems); - const mono_elem_span = mtuple.elems; - if (elem_vars.len != mono_elem_span.len) { + const mono_elems = self.store.monotype_store.tupleElems(resolved); + if (elem_vars.len != mono_elems.len) { typeBindingInvariant( "bindFlatTypeMonotypes(tuple): arity mismatch (type={d}, monotype={d})", - .{ elem_vars.len, mono_elem_span.len }, + .{ elem_vars.len, mono_elems.len }, ); } for (elem_vars, 0..) |ev, i| { - const me = self.store.monotype_store.getIdxSpan(mono_elem_span)[i]; - try self.bindTypeVarMonotypes(ev, me); + try self.bindTypeVarMonotypes(ev, mono_elems[i]); } }, .tag_union => |tag_union_row| { - const mono_tag_span = switch (mono) { - .tag_union => |mtu| mtu.tags, - else => typeBindingInvariant( - "bindFlatTypeMonotypes(tag_union): expected tag_union monotype, found '{s}'", - .{@tagName(mono)}, - ), - }; - const mono_tags = self.store.monotype_store.getTags(mono_tag_span); + if (resolved.kind != .tag_union) { + typeBindingInvariant( + "bindFlatTypeMonotypes(tag_union): expected tag_union monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } + const mono_tags = self.store.monotype_store.tagUnionTags(resolved); var seen_tag_indices: std.ArrayListUnmanaged(u32) = .empty; defer seen_tag_indices.deinit(self.allocator); @@ -6289,37 +6095,36 @@ fn bindFlatTypeMonotypes(self: *Self, flat_type: types.FlatType, monotype: Monot if (!seenIndex(seen_tag_indices.items, @intCast(tag_idx))) { typeBindingInvariant( "bindFlatTypeMonotypes(tag_union): monotype tag '{s}' missing from type row", - .{module_env.getIdent(mono_tag.name)}, + .{self.store.monotype_store.getNameText(mono_tag.name)}, ); } } }, .empty_record => { - switch (mono) { - .unit => {}, - .record => |mrec| { - const fields = self.store.monotype_store.getFields(mrec.fields); - if (fields.len != 0) { - typeBindingInvariant( - "bindFlatTypeMonotypes(empty_record): expected zero record fields, found {d}", - .{fields.len}, - ); - } - }, - else => typeBindingInvariant( - "bindFlatTypeMonotypes(empty_record): expected unit/empty-record monotype, found '{s}'", - .{@tagName(mono)}, - ), + if (!resolved.isUnit() and resolved.kind != .record) { + typeBindingInvariant( + "bindFlatTypeMonotypes(empty_record): expected unit/empty-record monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } + if (resolved.kind == .record) { + const fields = self.store.monotype_store.recordFields(resolved); + if (fields.len != 0) { + typeBindingInvariant( + "bindFlatTypeMonotypes(empty_record): expected zero record fields, found {d}", + .{fields.len}, + ); + } } }, .empty_tag_union => { - const mono_tags = switch (mono) { - .tag_union => |mtu| self.store.monotype_store.getTags(mtu.tags), - else => typeBindingInvariant( - "bindFlatTypeMonotypes(empty_tag_union): expected empty tag union monotype, found '{s}'", - .{@tagName(mono)}, - ), - }; + if (resolved.kind != .tag_union) { + typeBindingInvariant( + "bindFlatTypeMonotypes(empty_tag_union): expected empty tag union monotype, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } + const mono_tags = self.store.monotype_store.tagUnionTags(resolved); if (mono_tags.len != 0) { typeBindingInvariant( "bindFlatTypeMonotypes(empty_tag_union): expected zero tags, found {d}", diff --git a/src/mir/Monotype.zig b/src/mir/Monotype.zig index 3e0c5838b77..86bb6e418c1 100644 --- a/src/mir/Monotype.zig +++ b/src/mir/Monotype.zig @@ -1,11 +1,8 @@ //! Monomorphic type system for MIR. //! -//! Monotypes are fully concrete types with no type variables, no extensions, -//! no aliases, and no nominal/opaque/structural distinction. Records, tag unions, -//! and tuples are just records, tag unions, and tuples — the distinctions that -//! existed for static dispatch and module boundary enforcement are no longer needed. -//! -//! Each MIR expression has exactly one Monotype via a 1:1 Expr.Idx → Monotype.Idx mapping. +//! Types are fully concrete — no type variables, extensions, aliases, or +//! nominal/opaque/structural distinction. The TypeInterner provides canonical +//! type identity: structurally equal types always share the same TypeId. const std = @import("std"); const base = @import("base"); @@ -18,93 +15,91 @@ const ModuleEnv = can.ModuleEnv; const CommonIdents = can.ModuleEnv.CommonIdents; const StaticDispatchConstraint = types.StaticDispatchConstraint; -/// Check if a constraint range contains a numeric constraint (from_numeral, -/// desugared_binop, or desugared_unaryop). These imply the type variable is -/// numeric and should default to Dec rather than unit. -fn hasNumeralConstraint(types_store: *const types.Store, constraints: StaticDispatchConstraint.SafeList.Range) bool { - if (constraints.isEmpty()) return false; - for (types_store.sliceStaticDispatchConstraints(constraints)) |constraint| { - switch (constraint.origin) { - .from_numeral, .desugared_binop, .desugared_unaryop => return true, - .method_call, .where_clause => {}, - } +// ═══════════════════════════════════════════════════════════════ +// Type Handles +// ═══════════════════════════════════════════════════════════════ + +/// Canonical type handle — equality IS semantic type equality. +/// 32-bit packed: 29-bit per-kind index + 3-bit kind tag. +pub const TypeId = packed struct(u32) { + index: u29, + kind: Kind, + + pub const Kind = enum(u3) { + builtin, // unit + all Prim variants (encoded in index) + list, // payload: TypeId (elem) + box, // payload: TypeId (inner) + tuple, // payload: IdxListId + record, // payload: FieldListId + tag_union, // payload: TagListId + func, // payload: FuncPayload + rec, // recursive type indirection + }; + + pub const none: TypeId = .{ .index = std.math.maxInt(u29), .kind = .builtin }; + + pub fn isNone(self: TypeId) bool { + return @as(u32, @bitCast(self)) == @as(u32, @bitCast(none)); } - return false; -} -/// Index into the Store's monotypes array. -/// Since MIR has a 1:1 expr-to-type mapping, an Expr.Idx can be directly -/// reinterpreted as a Monotype.Idx. -pub const Idx = enum(u32) { - _, + pub fn eql(a: TypeId, b: TypeId) bool { + return @as(u32, @bitCast(a)) == @as(u32, @bitCast(b)); + } - pub const none: Idx = @enumFromInt(std.math.maxInt(u32)); + /// True if this TypeId represents the unit type (builtin index 0). + pub fn isUnit(self: TypeId) bool { + return self.kind == .builtin and self.index == 0; + } - pub fn isNone(self: Idx) bool { - return self == none; + /// If this TypeId is a primitive, return which one. + pub fn builtinPrim(self: TypeId) ?Prim { + if (self.kind != .builtin or self.index == 0) return null; + const prim_count = @typeInfo(Prim).@"enum".fields.len; + if (self.index > prim_count) return null; + return @enumFromInt(self.index - 1); } }; -/// Span of Monotype.Idx values stored in the extra_data array. -pub const Span = extern struct { - start: u32, - len: u16, +/// Backward-compat alias so existing `Monotype.Idx` references compile. +pub const Idx = TypeId; - pub fn empty() Span { - return .{ .start = 0, .len = 0 }; +/// Canonical list of TypeIds (function args, tuple elems, tag payloads). +pub const IdxListId = enum(u32) { + empty = 0, + _, + + pub fn isEmpty(self: IdxListId) bool { + return self == .empty; } +}; + +/// Canonical list of record fields. +pub const FieldListId = enum(u32) { + empty = 0, + _, - pub fn isEmpty(self: Span) bool { - return self.len == 0; + pub fn isEmpty(self: FieldListId) bool { + return self == .empty; } }; -/// A monomorphic type — fully concrete, no type variables, no extensions. -pub const Monotype = union(enum) { - /// Function type: args -> ret - func: struct { - args: Span, - ret: Idx, - effectful: bool, - }, - - /// Closed tag union (tags sorted by name) - tag_union: struct { - tags: TagSpan, - }, - - /// Closed record (fields sorted by name) - record: struct { - fields: FieldSpan, - }, - - /// Tuple - tuple: struct { - elems: Span, - }, - - /// List with element type - list: struct { - elem: Idx, - }, - - /// Primitive type - prim: Prim, - - /// Box (heap-allocated wrapper) - box: struct { - inner: Idx, - }, - - /// Unit / empty record - unit: void, - - /// Temporary placeholder used during recursive type construction. - /// Must be overwritten before construction completes; surviving - /// placeholders indicate a bug in fromFlatType/fromFuncType/fromNominalType. - recursive_placeholder: void, +/// Canonical list of tag-union tags. +pub const TagListId = enum(u32) { + empty = 0, + _, + + pub fn isEmpty(self: TagListId) bool { + return self == .empty; + } }; +/// Canonical structural name (record field names, tag names). +pub const StructuralNameId = enum(u32) { _ }; + +// ═══════════════════════════════════════════════════════════════ +// Payload / Key Types +// ═══════════════════════════════════════════════════════════════ + /// Primitive type kinds. pub const Prim = enum { str, @@ -123,188 +118,207 @@ pub const Prim = enum { dec, }; -/// A tag in a tag union. -pub const Tag = struct { - name: Ident.Idx, - /// Span of Monotype.Idx for payload types - payloads: Span, +/// A record field key: canonical name + canonical type. +pub const FieldKey = struct { + name: StructuralNameId, + ty: TypeId, - pub fn sortByNameAsc(ident_store: *const Ident.Store, a: Tag, b: Tag) bool { - return orderByName(ident_store, a, b) == .lt; + pub fn sortByNameAsc(pool: *const StructuralNamePool, a: FieldKey, b: FieldKey) bool { + return std.mem.order(u8, pool.getText(a.name), pool.getText(b.name)) == .lt; } +}; + +/// A tag-union tag key: canonical name + canonical payload list. +pub const TagKey = struct { + name: StructuralNameId, + payloads: IdxListId, - fn orderByName(ident_store: *const Ident.Store, a: Tag, b: Tag) std.math.Order { - const a_text = ident_store.getText(a.name); - const b_text = ident_store.getText(b.name); - return std.mem.order(u8, a_text, b_text); + pub fn sortByNameAsc(pool: *const StructuralNamePool, a: TagKey, b: TagKey) bool { + return std.mem.order(u8, pool.getText(a.name), pool.getText(b.name)) == .lt; } }; -/// Span of Tags stored in the tags array. -pub const TagSpan = extern struct { - start: u32, - len: u16, +/// Function type payload: canonical arg list + return type + effectfulness. +pub const FuncPayload = struct { + args: IdxListId, + ret: TypeId, + effectful: bool, +}; - pub fn empty() TagSpan { - return .{ .start = 0, .len = 0 }; - } +// ═══════════════════════════════════════════════════════════════ +// Structural Name Pool +// ═══════════════════════════════════════════════════════════════ + +/// Compilation-wide pool for canonical structural field/tag names. +pub const StructuralNamePool = struct { + alloc: Allocator, + map: std.StringHashMapUnmanaged(StructuralNameId), + texts: std.ArrayListUnmanaged([]const u8), - pub fn isEmpty(self: TagSpan) bool { - return self.len == 0; + fn init(allocator: Allocator) StructuralNamePool { + return .{ .alloc = allocator, .map = .{}, .texts = .empty }; } -}; -/// A field in a record. -pub const Field = struct { - name: Ident.Idx, - type_idx: Idx, + fn deinit(self: *StructuralNamePool) void { + for (self.texts.items) |text| self.alloc.free(text); + self.texts.deinit(self.alloc); + self.map.deinit(self.alloc); + } - pub fn sortByNameAsc(ident_store: *const Ident.Store, a: Field, b: Field) bool { - return orderByName(ident_store, a, b) == .lt; + pub fn intern(self: *StructuralNamePool, text: []const u8) !StructuralNameId { + const gop = try self.map.getOrPut(self.alloc, text); + if (gop.found_existing) return gop.value_ptr.*; + const owned = try self.alloc.dupe(u8, text); + const id: StructuralNameId = @enumFromInt(self.texts.items.len); + try self.texts.append(self.alloc, owned); + gop.key_ptr.* = owned; + gop.value_ptr.* = id; + return id; } - fn orderByName(ident_store: *const Ident.Store, a: Field, b: Field) std.math.Order { - const a_text = ident_store.getText(a.name); - const b_text = ident_store.getText(b.name); - return std.mem.order(u8, a_text, b_text); + pub fn getText(self: *const StructuralNamePool, id: StructuralNameId) []const u8 { + return self.texts.items[@intFromEnum(id)]; } }; -/// Span of Fields stored in the fields array. -pub const FieldSpan = extern struct { - start: u32, - len: u16, +// ═══════════════════════════════════════════════════════════════ +// Internal helpers +// ═══════════════════════════════════════════════════════════════ - pub fn empty() FieldSpan { - return .{ .start = 0, .len = 0 }; +fn hasNumeralConstraint(types_store: *const types.Store, constraints: StaticDispatchConstraint.SafeList.Range) bool { + if (constraints.isEmpty()) return false; + for (types_store.sliceStaticDispatchConstraints(constraints)) |constraint| { + switch (constraint.origin) { + .from_numeral, .desugared_binop, .desugared_unaryop => return true, + .method_call, .where_clause => {}, + } } + return false; +} + +/// Hash a slice of TypeIds for list interning. +fn hashTypeIds(items: []const TypeId) u64 { + var h = std.hash.Wyhash.init(0); + for (items) |item| std.hash.autoHash(&h, @as(u32, @bitCast(item))); + std.hash.autoHash(&h, items.len); + return h.final(); +} - pub fn isEmpty(self: FieldSpan) bool { - return self.len == 0; +/// Hash a slice of FieldKeys for field-list interning. +fn hashFieldKeys(items: []const FieldKey) u64 { + var h = std.hash.Wyhash.init(1); + for (items) |item| { + std.hash.autoHash(&h, @intFromEnum(item.name)); + std.hash.autoHash(&h, @as(u32, @bitCast(item.ty))); } -}; + std.hash.autoHash(&h, items.len); + return h.final(); +} + +/// Hash a slice of TagKeys for tag-list interning. +fn hashTagKeys(items: []const TagKey) u64 { + var h = std.hash.Wyhash.init(2); + for (items) |item| { + std.hash.autoHash(&h, @intFromEnum(item.name)); + std.hash.autoHash(&h, @intFromEnum(item.payloads)); + } + std.hash.autoHash(&h, items.len); + return h.final(); +} + +/// Compare two TypeId slices for equality. +fn eqlTypeIds(a: []const TypeId, b: []const TypeId) bool { + if (a.len != b.len) return false; + return std.mem.eql(u8, std.mem.sliceAsBytes(a), std.mem.sliceAsBytes(b)); +} + +/// Compare two FieldKey slices for equality. +fn eqlFieldKeys(a: []const FieldKey, b: []const FieldKey) bool { + if (a.len != b.len) return false; + return std.mem.eql(u8, std.mem.sliceAsBytes(a), std.mem.sliceAsBytes(b)); +} + +/// Compare two TagKey slices for equality. +fn eqlTagKeys(a: []const TagKey, b: []const TagKey) bool { + if (a.len != b.len) return false; + return std.mem.eql(u8, std.mem.sliceAsBytes(a), std.mem.sliceAsBytes(b)); +} const NamedSpecialization = struct { name_text: []const u8, - type_idx: Idx, + type_idx: TypeId, }; -/// Flat storage for monomorphic types. -pub const Store = struct { - monotypes: std.ArrayListUnmanaged(Monotype), - /// Monotype.Idx values for spans (func args, tuple elems) - extra_idx: std.ArrayListUnmanaged(u32), - tags: std.ArrayListUnmanaged(Tag), - fields: std.ArrayListUnmanaged(Field), - - /// Pre-interned index for the unit monotype. - unit_idx: Idx, - /// Pre-interned indices for each primitive monotype, indexed by `@intFromEnum(Prim)`. - prim_idxs: [prim_count]Idx, - /// Cached ordinary tag-union monotype for nominal Bool. - bool_tag_union_idx: Idx, - - intern_map: InternMap, +// ═══════════════════════════════════════════════════════════════ +// TypeInterner +// ═══════════════════════════════════════════════════════════════ + +/// Compilation-wide canonical type interner. Every structurally equal +/// monotype receives exactly one TypeId. +pub const TypeInterner = struct { + allocator: Allocator, + + // Per-kind payload arenas (indexed by TypeId.index for that kind) + list_payloads: std.ArrayListUnmanaged(TypeId), // elem type + box_payloads: std.ArrayListUnmanaged(TypeId), // inner type + tuple_payloads: std.ArrayListUnmanaged(IdxListId), + record_payloads: std.ArrayListUnmanaged(FieldListId), + union_payloads: std.ArrayListUnmanaged(TagListId), + func_payloads: std.ArrayListUnmanaged(FuncPayload), + rec_targets: std.ArrayListUnmanaged(TypeId), // recursive indirection + + // Canonical list storage + idx_list_metas: std.ArrayListUnmanaged(IdxListMeta), + idx_list_items: std.ArrayListUnmanaged(TypeId), + field_list_metas: std.ArrayListUnmanaged(FieldListMeta), + field_list_items: std.ArrayListUnmanaged(FieldKey), + tag_list_metas: std.ArrayListUnmanaged(TagListMeta), + tag_list_items: std.ArrayListUnmanaged(TagKey), + + // Per-kind intern maps (canonical key → TypeId) + list_map: std.AutoHashMapUnmanaged(u32, TypeId), // @bitCast(elem TypeId) + box_map: std.AutoHashMapUnmanaged(u32, TypeId), // @bitCast(inner TypeId) + tuple_map: std.AutoHashMapUnmanaged(IdxListId, TypeId), + record_map: std.AutoHashMapUnmanaged(FieldListId, TypeId), + union_map: std.AutoHashMapUnmanaged(TagListId, TypeId), + func_map: std.AutoHashMapUnmanaged(u96, TypeId), // packed func key + + // Structural name pool + name_pool: StructuralNamePool, + + // Pre-interned builtins + unit_idx: TypeId, + prim_idxs: [prim_count]TypeId, + bool_tag_union_idx: TypeId, const prim_count = @typeInfo(Prim).@"enum".fields.len; - const ContentContext = struct { - store: *const Store, - - pub fn hash(ctx: ContentContext, mono: Monotype) u32 { - var hasher = std.hash.Wyhash.init(0); - std.hash.autoHash(&hasher, std.meta.activeTag(mono)); - - switch (mono) { - .unit, .recursive_placeholder => {}, - .prim => |p| std.hash.autoHash(&hasher, p), - .list => |l| std.hash.autoHash(&hasher, l.elem), - .box => |b| std.hash.autoHash(&hasher, b.inner), - .func => |f| { - std.hash.autoHash(&hasher, f.effectful); - std.hash.autoHash(&hasher, f.ret); - for (ctx.store.getIdxSpan(f.args)) |arg| std.hash.autoHash(&hasher, arg); - }, - .tuple => |t| { - for (ctx.store.getIdxSpan(t.elems)) |elem| std.hash.autoHash(&hasher, elem); - }, - .record => |r| { - for (ctx.store.getFields(r.fields)) |field| { - std.hash.autoHash(&hasher, field.name); - std.hash.autoHash(&hasher, field.type_idx); - } - }, - .tag_union => |tu| { - for (ctx.store.getTags(tu.tags)) |tag| { - std.hash.autoHash(&hasher, tag.name); - for (ctx.store.getIdxSpan(tag.payloads)) |p| std.hash.autoHash(&hasher, p); - } - }, - } - - return @truncate(hasher.final()); - } + // --- Internal list metadata --- - pub fn eql(ctx: ContentContext, a: Monotype, b: Monotype) bool { - const a_tag = std.meta.activeTag(a); - const b_tag = std.meta.activeTag(b); - if (a_tag != b_tag) return false; - - return switch (a) { - .unit, .recursive_placeholder => true, - .prim => |p| p == b.prim, - .list => |l| l.elem == b.list.elem, - .box => |bx| bx.inner == b.box.inner, - .func => |f| { - const bf = b.func; - if (f.effectful != bf.effectful) return false; - if (f.ret != bf.ret) return false; - return std.mem.eql(Idx, ctx.store.getIdxSpan(f.args), ctx.store.getIdxSpan(bf.args)); - }, - .tuple => |t| std.mem.eql(Idx, ctx.store.getIdxSpan(t.elems), ctx.store.getIdxSpan(b.tuple.elems)), - .record => |r| { - const a_fields = ctx.store.getFields(r.fields); - const b_fields = ctx.store.getFields(b.record.fields); - if (a_fields.len != b_fields.len) return false; - for (a_fields, b_fields) |af, bf| { - if (af.name != bf.name or af.type_idx != bf.type_idx) return false; - } - return true; - }, - .tag_union => |tu| { - const a_tags = ctx.store.getTags(tu.tags); - const b_tags = ctx.store.getTags(b.tag_union.tags); - if (a_tags.len != b_tags.len) return false; - for (a_tags, b_tags) |at, bt| { - if (at.name != bt.name) return false; - if (!std.mem.eql(Idx, ctx.store.getIdxSpan(at.payloads), ctx.store.getIdxSpan(bt.payloads))) return false; - } - return true; - }, - }; - } - }; + const IdxListMeta = struct { start: u32, len: u32, fingerprint: u64 }; + const FieldListMeta = struct { start: u32, len: u32, fingerprint: u64 }; + const TagListMeta = struct { start: u32, len: u32, fingerprint: u64 }; - const InternMap = std.HashMapUnmanaged(Monotype, Idx, ContentContext, std.hash_map.default_max_load_percentage); + // --- Scratches (reusable temporary buffers) --- pub const Scratches = struct { - fields: base.Scratch(Field), - tags: base.Scratch(Tag), - idxs: base.Scratch(Idx), + fields: base.Scratch(FieldKey), + tags: base.Scratch(TagKey), + idxs: base.Scratch(TypeId), named_specializations: base.Scratch(NamedSpecialization), - /// Ident store for sorting tag names alphabetically. + /// Ident store for resolving tag/field name text. /// Updated when switching modules during cross-module lowering. ident_store: ?*const Ident.Store = null, - /// Module env owning the current `types_store` / `ident_store`. + /// Module env owning the current types_store / ident_store. module_env: ?*const ModuleEnv = null, - /// Shared module env slice used to resolve nominal definitions by origin module. + /// Shared module env slice for resolving nominal definitions. all_module_envs: ?[]const *ModuleEnv = null, pub fn init(allocator: Allocator) Allocator.Error!Scratches { return .{ - .fields = try base.Scratch(Field).init(allocator), - .tags = try base.Scratch(Tag).init(allocator), - .idxs = try base.Scratch(Idx).init(allocator), + .fields = try base.Scratch(FieldKey).init(allocator), + .tags = try base.Scratch(TagKey).init(allocator), + .idxs = try base.Scratch(TypeId).init(allocator), .named_specializations = try base.Scratch(NamedSpecialization).init(allocator), }; } @@ -317,146 +331,307 @@ pub const Store = struct { } }; - /// Look up the pre-interned index for a primitive type. - pub fn primIdx(self: *const Store, prim: Prim) Idx { + pub fn init(allocator: Allocator) Allocator.Error!TypeInterner { + var self = TypeInterner{ + .allocator = allocator, + .list_payloads = .empty, + .box_payloads = .empty, + .tuple_payloads = .empty, + .record_payloads = .empty, + .union_payloads = .empty, + .func_payloads = .empty, + .rec_targets = .empty, + .idx_list_metas = .empty, + .idx_list_items = .empty, + .field_list_metas = .empty, + .field_list_items = .empty, + .tag_list_metas = .empty, + .tag_list_items = .empty, + .list_map = .{}, + .box_map = .{}, + .tuple_map = .{}, + .record_map = .{}, + .union_map = .{}, + .func_map = .{}, + .name_pool = StructuralNamePool.init(allocator), + .unit_idx = undefined, + .prim_idxs = undefined, + .bool_tag_union_idx = TypeId.none, + }; + + // Pre-intern unit (builtin index 0) and prims (builtin indices 1..14). + self.unit_idx = TypeId{ .kind = .builtin, .index = 0 }; + for (0..prim_count) |i| { + self.prim_idxs[i] = TypeId{ .kind = .builtin, .index = @intCast(i + 1) }; + } + + return self; + } + + pub fn deinit(self: *TypeInterner, allocator: Allocator) void { + _ = allocator; // TypeInterner uses self.allocator + self.list_payloads.deinit(self.allocator); + self.box_payloads.deinit(self.allocator); + self.tuple_payloads.deinit(self.allocator); + self.record_payloads.deinit(self.allocator); + self.union_payloads.deinit(self.allocator); + self.func_payloads.deinit(self.allocator); + self.rec_targets.deinit(self.allocator); + self.idx_list_metas.deinit(self.allocator); + self.idx_list_items.deinit(self.allocator); + self.field_list_metas.deinit(self.allocator); + self.field_list_items.deinit(self.allocator); + self.tag_list_metas.deinit(self.allocator); + self.tag_list_items.deinit(self.allocator); + self.list_map.deinit(self.allocator); + self.box_map.deinit(self.allocator); + self.tuple_map.deinit(self.allocator); + self.record_map.deinit(self.allocator); + self.union_map.deinit(self.allocator); + self.func_map.deinit(self.allocator); + self.name_pool.deinit(); + } + + /// Look up the pre-interned TypeId for a primitive type. + pub fn primIdx(self: *const TypeInterner, prim: Prim) TypeId { return self.prim_idxs[@intFromEnum(prim)]; } - pub fn addBoolTagUnion(self: *Store, allocator: Allocator, common_idents: CommonIdents) !Idx { + /// Get or create the Bool tag union ([False, True]). + pub fn addBoolTagUnion(self: *TypeInterner) !TypeId { if (!self.bool_tag_union_idx.isNone()) return self.bool_tag_union_idx; + const false_name = try self.name_pool.intern("False"); + const true_name = try self.name_pool.intern("True"); + const result = try self.internTagUnion(&.{ + .{ .name = false_name, .payloads = .empty }, + .{ .name = true_name, .payloads = .empty }, + }); + self.bool_tag_union_idx = result; + return result; + } + + pub fn internList(self: *TypeInterner, elem: TypeId) !TypeId { + const key: u32 = @bitCast(elem); + if (self.list_map.get(key)) |existing| return existing; + const index: u29 = @intCast(self.list_payloads.items.len); + try self.list_payloads.append(self.allocator, elem); + const id = TypeId{ .kind = .list, .index = index }; + try self.list_map.put(self.allocator, key, id); + return id; + } + + pub fn internBox(self: *TypeInterner, inner: TypeId) !TypeId { + const key: u32 = @bitCast(inner); + if (self.box_map.get(key)) |existing| return existing; + const index: u29 = @intCast(self.box_payloads.items.len); + try self.box_payloads.append(self.allocator, inner); + const id = TypeId{ .kind = .box, .index = index }; + try self.box_map.put(self.allocator, key, id); + return id; + } - const false_payloads = Span.empty(); - const true_payloads = Span.empty(); - const tags = try self.addTags(allocator, &.{ - .{ .name = common_idents.false_tag, .payloads = false_payloads }, - .{ .name = common_idents.true_tag, .payloads = true_payloads }, + pub fn internTuple(self: *TypeInterner, elems: []const TypeId) !TypeId { + const elem_list = try self.internIdxList(elems); + if (self.tuple_map.get(elem_list)) |existing| return existing; + const index: u29 = @intCast(self.tuple_payloads.items.len); + try self.tuple_payloads.append(self.allocator, elem_list); + const id = TypeId{ .kind = .tuple, .index = index }; + try self.tuple_map.put(self.allocator, elem_list, id); + return id; + } + + pub fn internFunc(self: *TypeInterner, args: []const TypeId, ret: TypeId, effectful: bool) !TypeId { + const arg_list = try self.internIdxList(args); + const key = packFuncKey(arg_list, ret, effectful); + if (self.func_map.get(key)) |existing| return existing; + const index: u29 = @intCast(self.func_payloads.items.len); + try self.func_payloads.append(self.allocator, .{ + .args = arg_list, + .ret = ret, + .effectful = effectful, }); - const idx = try self.addMonotype(allocator, .{ .tag_union = .{ .tags = tags } }); - self.bool_tag_union_idx = idx; - return idx; + const id = TypeId{ .kind = .func, .index = index }; + try self.func_map.put(self.allocator, key, id); + return id; } - /// Pre-populate the store with the 16 fixed monotypes (unit + 15 primitives). - pub fn init(allocator: Allocator) Allocator.Error!Store { - var store = Store{ - .monotypes = .empty, - .extra_idx = .empty, - .tags = .empty, - .fields = .empty, - .intern_map = .{}, - .unit_idx = undefined, - .prim_idxs = undefined, - .bool_tag_union_idx = .none, - }; + pub fn internRecord(self: *TypeInterner, fields: []const FieldKey) !TypeId { + const field_list = try self.internFieldList(fields); + if (self.record_map.get(field_list)) |existing| return existing; + const index: u29 = @intCast(self.record_payloads.items.len); + try self.record_payloads.append(self.allocator, field_list); + const id = TypeId{ .kind = .record, .index = index }; + try self.record_map.put(self.allocator, field_list, id); + return id; + } - store.unit_idx = try store.addMonotype(allocator, .unit); - for (0..prim_count) |i| { - store.prim_idxs[i] = try store.addMonotype(allocator, .{ .prim = @enumFromInt(i) }); - } + pub fn internTagUnion(self: *TypeInterner, tag_keys: []const TagKey) !TypeId { + const tag_list = try self.internTagList(tag_keys); + if (self.union_map.get(tag_list)) |existing| return existing; + const index: u29 = @intCast(self.union_payloads.items.len); + try self.union_payloads.append(self.allocator, tag_list); + const id = TypeId{ .kind = .tag_union, .index = index }; + try self.union_map.put(self.allocator, tag_list, id); + return id; + } - return store; + fn packFuncKey(args: IdxListId, ret: TypeId, effectful: bool) u96 { + return @as(u96, @intFromEnum(args)) | + (@as(u96, @as(u32, @bitCast(ret))) << 32) | + (@as(u96, @intFromBool(effectful)) << 64); } - pub fn deinit(self: *Store, allocator: Allocator) void { - self.intern_map.deinit(allocator); - self.monotypes.deinit(allocator); - self.extra_idx.deinit(allocator); - self.tags.deinit(allocator); - self.fields.deinit(allocator); + pub fn internIdxList(self: *TypeInterner, items: []const TypeId) !IdxListId { + if (items.len == 0) return .empty; + const fp = hashTypeIds(items); + for (self.idx_list_metas.items, 0..) |meta, i| { + if (meta.fingerprint != fp or meta.len != items.len) continue; + if (eqlTypeIds(self.idx_list_items.items[meta.start..][0..meta.len], items)) + return @enumFromInt(@as(u32, @intCast(i)) + 1); + } + const start: u32 = @intCast(self.idx_list_items.items.len); + try self.idx_list_items.appendSlice(self.allocator, items); + try self.idx_list_metas.append(self.allocator, .{ .start = start, .len = @intCast(items.len), .fingerprint = fp }); + return @enumFromInt(@as(u32, @intCast(self.idx_list_metas.items.len))); } - pub fn addMonotype(self: *Store, allocator: Allocator, mono: Monotype) !Idx { - if (mono == .recursive_placeholder) { - const idx: u32 = @intCast(self.monotypes.items.len); - try self.monotypes.append(allocator, mono); - return @enumFromInt(idx); + pub fn internFieldList(self: *TypeInterner, items: []const FieldKey) !FieldListId { + if (items.len == 0) return .empty; + const fp = hashFieldKeys(items); + for (self.field_list_metas.items, 0..) |meta, i| { + if (meta.fingerprint != fp or meta.len != items.len) continue; + if (eqlFieldKeys(self.field_list_items.items[meta.start..][0..meta.len], items)) + return @enumFromInt(@as(u32, @intCast(i)) + 1); } + const start: u32 = @intCast(self.field_list_items.items.len); + try self.field_list_items.appendSlice(self.allocator, items); + try self.field_list_metas.append(self.allocator, .{ .start = start, .len = @intCast(items.len), .fingerprint = fp }); + return @enumFromInt(@as(u32, @intCast(self.field_list_metas.items.len))); + } - const ctx = ContentContext{ .store = self }; - const gop = try self.intern_map.getOrPutContext(allocator, mono, ctx); - if (gop.found_existing) return gop.value_ptr.*; + pub fn internTagList(self: *TypeInterner, items: []const TagKey) !TagListId { + if (items.len == 0) return .empty; + const fp = hashTagKeys(items); + for (self.tag_list_metas.items, 0..) |meta, i| { + if (meta.fingerprint != fp or meta.len != items.len) continue; + if (eqlTagKeys(self.tag_list_items.items[meta.start..][0..meta.len], items)) + return @enumFromInt(@as(u32, @intCast(i)) + 1); + } + const start: u32 = @intCast(self.tag_list_items.items.len); + try self.tag_list_items.appendSlice(self.allocator, items); + try self.tag_list_metas.append(self.allocator, .{ .start = start, .len = @intCast(items.len), .fingerprint = fp }); + return @enumFromInt(@as(u32, @intCast(self.tag_list_metas.items.len))); + } - const idx: u32 = @intCast(self.monotypes.items.len); - try self.monotypes.append(allocator, mono); - const result: Idx = @enumFromInt(idx); - gop.value_ptr.* = result; - return result; + pub fn getIdxListItems(self: *const TypeInterner, id: IdxListId) []const TypeId { + if (id == .empty) return &.{}; + const meta = self.idx_list_metas.items[@intFromEnum(id) - 1]; + return self.idx_list_items.items[meta.start..][0..meta.len]; } - pub fn getMonotype(self: *const Store, idx: Idx) Monotype { - return self.monotypes.items[@intFromEnum(idx)]; + pub fn getFieldListItems(self: *const TypeInterner, id: FieldListId) []const FieldKey { + if (id == .empty) return &.{}; + const meta = self.field_list_metas.items[@intFromEnum(id) - 1]; + return self.field_list_items.items[meta.start..][0..meta.len]; } - /// Add a span of Monotype.Idx values to extra_idx and return a Span. - pub fn addIdxSpan(self: *Store, allocator: Allocator, ids: []const Idx) !Span { - if (ids.len == 0) return Span.empty(); - const start: u32 = @intCast(self.extra_idx.items.len); - for (ids) |id| { - try self.extra_idx.append(allocator, @intFromEnum(id)); - } - return .{ .start = start, .len = @intCast(ids.len) }; + pub fn getTagListItems(self: *const TypeInterner, id: TagListId) []const TagKey { + if (id == .empty) return &.{}; + const meta = self.tag_list_metas.items[@intFromEnum(id) - 1]; + return self.tag_list_items.items[meta.start..][0..meta.len]; } - /// Get a slice of Monotype.Idx from a Span. - pub fn getIdxSpan(self: *const Store, span: Span) []const Idx { - if (span.len == 0) return &.{}; - const raw = self.extra_idx.items[span.start..][0..span.len]; - return @ptrCast(raw); + pub fn listElem(self: *const TypeInterner, id: TypeId) TypeId { + std.debug.assert(id.kind == .list); + return self.list_payloads.items[id.index]; } - /// Add tags to the tags array and return a TagSpan. - pub fn addTags(self: *Store, allocator: Allocator, tag_slice: []const Tag) !TagSpan { - if (tag_slice.len == 0) return TagSpan.empty(); - const start: u32 = @intCast(self.tags.items.len); - try self.tags.appendSlice(allocator, tag_slice); - return .{ .start = start, .len = @intCast(tag_slice.len) }; + pub fn boxInner(self: *const TypeInterner, id: TypeId) TypeId { + std.debug.assert(id.kind == .box); + return self.box_payloads.items[id.index]; } - /// Get a slice of Tags from a TagSpan. - pub fn getTags(self: *const Store, span: TagSpan) []const Tag { - if (span.len == 0) return &.{}; - return self.tags.items[span.start..][0..span.len]; + pub fn tupleElems(self: *const TypeInterner, id: TypeId) []const TypeId { + std.debug.assert(id.kind == .tuple); + return self.getIdxListItems(self.tuple_payloads.items[id.index]); } - /// Add fields to the fields array and return a FieldSpan. - pub fn addFields(self: *Store, allocator: Allocator, field_slice: []const Field) !FieldSpan { - if (field_slice.len == 0) return FieldSpan.empty(); - const start: u32 = @intCast(self.fields.items.len); - try self.fields.appendSlice(allocator, field_slice); - return .{ .start = start, .len = @intCast(field_slice.len) }; + pub fn getFuncPayload(self: *const TypeInterner, id: TypeId) FuncPayload { + std.debug.assert(id.kind == .func); + return self.func_payloads.items[id.index]; } - /// Get a slice of Fields from a FieldSpan. - pub fn getFields(self: *const Store, span: FieldSpan) []const Field { - if (span.len == 0) return &.{}; - return self.fields.items[span.start..][0..span.len]; + pub fn funcArgs(self: *const TypeInterner, id: TypeId) []const TypeId { + return self.getIdxListItems(self.getFuncPayload(id).args); } - /// Convert a CIR type variable to a Monotype, recursively resolving all - /// type structure. - /// - /// `specializations` is a read-only map of type vars already bound to - /// concrete monotypes by `bindTypeVarMonotypes` (polymorphic specialization). - /// - /// Cycle detection for recursive nominal types (the only legitimate source - /// of cycles after type checking) is handled internally by `fromNominalType` - /// using `nominal_cycle_breakers`. + pub fn funcRet(self: *const TypeInterner, id: TypeId) TypeId { + return self.getFuncPayload(id).ret; + } + + pub fn funcEffectful(self: *const TypeInterner, id: TypeId) bool { + return self.getFuncPayload(id).effectful; + } + + pub fn recordFields(self: *const TypeInterner, id: TypeId) []const FieldKey { + std.debug.assert(id.kind == .record); + return self.getFieldListItems(self.record_payloads.items[id.index]); + } + + pub fn tagUnionTags(self: *const TypeInterner, id: TypeId) []const TagKey { + std.debug.assert(id.kind == .tag_union); + return self.getTagListItems(self.union_payloads.items[id.index]); + } + + /// Resolve a TypeId through any `.rec` indirection. + pub fn resolve(self: *const TypeInterner, id: TypeId) TypeId { + if (id.kind != .rec) return id; + const target = self.rec_targets.items[id.index]; + // At most one level of indirection + std.debug.assert(target.kind != .rec); + return target; + } + + /// Reserve a recursive type slot. Returns a `.rec` TypeId that can be + /// stored in cycle-breaker maps. Must be finalized later. + pub fn reserveRecursive(self: *TypeInterner) !TypeId { + const index: u29 = @intCast(self.rec_targets.items.len); + try self.rec_targets.append(self.allocator, TypeId.none); + return TypeId{ .kind = .rec, .index = index }; + } + + /// Finalize a reserved recursive slot with the actual canonical TypeId. + pub fn finalizeRecursive(self: *TypeInterner, rec_id: TypeId, actual: TypeId) void { + std.debug.assert(rec_id.kind == .rec); + self.rec_targets.items[rec_id.index] = actual; + } + + /// Intern a structural name from an Ident.Idx (resolves text via ident_store). + pub fn internNameFromIdent(self: *TypeInterner, ident_store: *const Ident.Store, ident: Ident.Idx) !StructuralNameId { + return try self.name_pool.intern(ident_store.getText(ident)); + } + + /// Get the text for a structural name. + pub fn getNameText(self: *const TypeInterner, id: StructuralNameId) []const u8 { + return self.name_pool.getText(id); + } + + /// Convert a CIR type variable to a canonical TypeId, recursively + /// resolving all type structure. pub fn fromTypeVar( - self: *Store, + self: *TypeInterner, allocator: Allocator, types_store: *const types.Store, type_var: types.Var, common_idents: CommonIdents, - specializations: *const std.AutoHashMap(types.Var, Idx), - nominal_cycle_breakers: *std.AutoHashMap(types.Var, Idx), + specializations: *const std.AutoHashMap(types.Var, TypeId), + nominal_cycle_breakers: *std.AutoHashMap(types.Var, TypeId), scratches: *Scratches, - ) Allocator.Error!Idx { + ) Allocator.Error!TypeId { + _ = allocator; // TypeInterner uses self.allocator const resolved = types_store.resolveVar(type_var); - // Check specialization bindings first (from bindTypeVarMonotypes). if (specializations.get(resolved.var_)) |cached| return cached; - - // Check nominal cycle breakers (for recursive nominal types like Tree := [Leaf, Node(Tree)]). if (nominal_cycle_breakers.get(resolved.var_)) |cached| return cached; return switch (resolved.desc.content) { @@ -475,46 +650,36 @@ pub const Store = struct { return self.unit_idx; }, .alias => |alias| { - // Aliases are transparent — follow the backing var const backing_var = types_store.getAliasBackingVar(alias); - return try self.fromTypeVar(allocator, types_store, backing_var, common_idents, specializations, nominal_cycle_breakers, scratches); + return try self.fromTypeVar(self.allocator, types_store, backing_var, common_idents, specializations, nominal_cycle_breakers, scratches); }, .structure => |flat_type| { - return try self.fromFlatType(allocator, types_store, resolved.var_, flat_type, common_idents, specializations, nominal_cycle_breakers, scratches); + return try self.fromFlatType(types_store, resolved.var_, flat_type, common_idents, specializations, nominal_cycle_breakers, scratches); }, - // Error types can appear nested inside function types (as argument - // or return types) when --allow-errors is used. The guard in - // lowerExpr only catches top-level error types, so we need to handle - // them here too. Return unit as a safe placeholder so lowering can - // continue and the runtime-error path is hit at runtime. .err => return self.unit_idx, }; } fn fromFlatType( - self: *Store, - allocator: Allocator, + self: *TypeInterner, types_store: *const types.Store, var_: types.Var, flat_type: types.FlatType, common_idents: CommonIdents, - specializations: *const std.AutoHashMap(types.Var, Idx), - nominal_cycle_breakers: *std.AutoHashMap(types.Var, Idx), + specializations: *const std.AutoHashMap(types.Var, TypeId), + nominal_cycle_breakers: *std.AutoHashMap(types.Var, TypeId), scratches: *Scratches, - ) Allocator.Error!Idx { + ) Allocator.Error!TypeId { return switch (flat_type) { .nominal_type => |nominal| { - return try self.fromNominalType(allocator, types_store, var_, nominal, common_idents, specializations, nominal_cycle_breakers, scratches); + return try self.fromNominalType(types_store, var_, nominal, common_idents, specializations, nominal_cycle_breakers, scratches); }, .empty_record => self.unit_idx, - .empty_tag_union => try self.addMonotype(allocator, .{ .tag_union = .{ .tags = TagSpan.empty() } }), + .empty_tag_union => try self.internTagUnion(&.{}), .record => |record| { const scratch_top = scratches.fields.top(); defer scratches.fields.clearFrom(scratch_top); - // Follow the record extension chain to collect ALL fields. - // Roc's type system represents records as linked rows: - // { a: A | ext } where ext -> { b: B | ext2 } where ext2 -> {} var current_row = record; rows: while (true) { const fields_slice = types_store.getRecordFieldsSlice(current_row.fields); @@ -524,15 +689,18 @@ pub const Store = struct { for (names, vars) |name, field_var| { var seen_name = false; for (scratches.fields.sliceFromStart(scratch_top)) |existing| { - if (existing.name.eql(name)) { + const existing_text = self.name_pool.getText(existing.name); + const name_text = scratches.ident_store.?.getText(name); + if (std.mem.eql(u8, existing_text, name_text)) { seen_name = true; break; } } if (seen_name) continue; - const field_type = try self.fromTypeVar(allocator, types_store, field_var, common_idents, specializations, nominal_cycle_breakers, scratches); - try scratches.fields.append(.{ .name = name, .type_idx = field_type }); + const field_type = try self.fromTypeVar(self.allocator, types_store, field_var, common_idents, specializations, nominal_cycle_breakers, scratches); + const sname = try self.internNameFromIdent(scratches.ident_store.?, name); + try scratches.fields.append(.{ .name = sname, .ty = field_type }); } var ext_var = current_row.ext; @@ -549,22 +717,24 @@ pub const Store = struct { continue :rows; }, .record_unbound => |fields_range| { - // Final open-row segment: append known fields. const ext_fields = types_store.getRecordFieldsSlice(fields_range); const ext_names = ext_fields.items(.name); const ext_vars = ext_fields.items(.var_); for (ext_names, ext_vars) |name, field_var| { var seen_name = false; for (scratches.fields.sliceFromStart(scratch_top)) |existing| { - if (existing.name.eql(name)) { + const existing_text = self.name_pool.getText(existing.name); + const name_text = scratches.ident_store.?.getText(name); + if (std.mem.eql(u8, existing_text, name_text)) { seen_name = true; break; } } if (seen_name) continue; - const field_type = try self.fromTypeVar(allocator, types_store, field_var, common_idents, specializations, nominal_cycle_breakers, scratches); - try scratches.fields.append(.{ .name = name, .type_idx = field_type }); + const field_type = try self.fromTypeVar(self.allocator, types_store, field_var, common_idents, specializations, nominal_cycle_breakers, scratches); + const sname = try self.internNameFromIdent(scratches.ident_store.?, name); + try scratches.fields.append(.{ .name = sname, .ty = field_type }); } break :rows; }, @@ -583,20 +753,17 @@ pub const Store = struct { if (findNamedRowExtensionMonotype(scratches, ext_var, types_store)) |specialized| { try self.appendSpecializedRecordFields(specialized, scratch_top, scratches); } - break :rows; // Open record — treat as closed with collected fields + break :rows; }, .rigid => { if (findNamedRowExtensionMonotype(scratches, ext_var, types_store)) |specialized| { try self.appendSpecializedRecordFields(specialized, scratch_top, scratches); } - break :rows; // Rigid record — treat as closed with collected fields + break :rows; }, .err => { if (std.debug.runtime_safety) { - std.debug.panic( - "Monotype.fromTypeVar(record): error row extension tail", - .{}, - ); + std.debug.panic("Monotype.fromTypeVar(record): error row extension tail", .{}); } unreachable; }, @@ -605,15 +772,10 @@ pub const Store = struct { } const collected_fields = scratches.fields.sliceFromStart(scratch_top); - // Each row segment is sorted, but concatenation may not be. - if (scratches.ident_store) |ident_store| { - std.mem.sort(Field, collected_fields, ident_store, Field.sortByNameAsc); - } - const field_span = try self.addFields(allocator, collected_fields); - return try self.addMonotype(allocator, .{ .record = .{ .fields = field_span } }); + std.mem.sort(FieldKey, collected_fields, &self.name_pool, FieldKey.sortByNameAsc); + return try self.internRecord(collected_fields); }, .record_unbound => |fields_range| { - // Extensible record — treat like a closed record with the known fields const fields_slice = types_store.getRecordFieldsSlice(fields_range); const names = fields_slice.items(.name); const vars = fields_slice.items(.var_); @@ -622,12 +784,14 @@ pub const Store = struct { defer scratches.fields.clearFrom(scratch_top); for (names, vars) |name, field_var| { - const field_type = try self.fromTypeVar(allocator, types_store, field_var, common_idents, specializations, nominal_cycle_breakers, scratches); - try scratches.fields.append(.{ .name = name, .type_idx = field_type }); + const field_type = try self.fromTypeVar(self.allocator, types_store, field_var, common_idents, specializations, nominal_cycle_breakers, scratches); + const sname = try self.internNameFromIdent(scratches.ident_store.?, name); + try scratches.fields.append(.{ .name = sname, .ty = field_type }); } - const field_span = try self.addFields(allocator, scratches.fields.sliceFromStart(scratch_top)); - return try self.addMonotype(allocator, .{ .record = .{ .fields = field_span } }); + const collected = scratches.fields.sliceFromStart(scratch_top); + std.mem.sort(FieldKey, collected, &self.name_pool, FieldKey.sortByNameAsc); + return try self.internRecord(collected); }, .tuple => |tuple| { const elem_vars = types_store.sliceVars(tuple.elems); @@ -635,20 +799,16 @@ pub const Store = struct { defer scratches.idxs.clearFrom(scratch_top); for (elem_vars) |elem_var| { - const elem_type = try self.fromTypeVar(allocator, types_store, elem_var, common_idents, specializations, nominal_cycle_breakers, scratches); + const elem_type = try self.fromTypeVar(self.allocator, types_store, elem_var, common_idents, specializations, nominal_cycle_breakers, scratches); try scratches.idxs.append(elem_type); } - const elem_span = try self.addIdxSpan(allocator, scratches.idxs.sliceFromStart(scratch_top)); - return try self.addMonotype(allocator, .{ .tuple = .{ .elems = elem_span } }); + return try self.internTuple(scratches.idxs.sliceFromStart(scratch_top)); }, .tag_union => |tag_union_row| { const tags_top = scratches.tags.top(); defer scratches.tags.clearFrom(tags_top); - // Follow the tag union extension chain to collect ALL tags. - // Roc's type system represents tag unions as linked rows: - // [Ok a | ext] where ext -> [Err b | ext2] where ext2 -> empty_tag_union var current_row = tag_union_row; rows: while (true) { const tags_slice = types_store.getTagsSlice(current_row.tags); @@ -661,15 +821,15 @@ pub const Store = struct { defer scratches.idxs.clearFrom(idxs_top); for (arg_vars) |arg_var| { - const payload_type = try self.fromTypeVar(allocator, types_store, arg_var, common_idents, specializations, nominal_cycle_breakers, scratches); + const payload_type = try self.fromTypeVar(self.allocator, types_store, arg_var, common_idents, specializations, nominal_cycle_breakers, scratches); try scratches.idxs.append(payload_type); } - const payloads_span = try self.addIdxSpan(allocator, scratches.idxs.sliceFromStart(idxs_top)); - try scratches.tags.append(.{ .name = name, .payloads = payloads_span }); + const payloads_list = try self.internIdxList(scratches.idxs.sliceFromStart(idxs_top)); + const sname = try self.internNameFromIdent(scratches.ident_store.?, name); + try scratches.tags.append(.{ .name = sname, .payloads = payloads_list }); } - // Follow extension variable to find more tags var ext_var = current_row.ext; while (true) { const ext_resolved = types_store.resolveVar(ext_var); @@ -698,20 +858,17 @@ pub const Store = struct { if (findNamedRowExtensionMonotype(scratches, ext_var, types_store)) |specialized| { try self.appendSpecializedTagUnionTags(specialized, scratches); } - break :rows; // Open tag union — treat as closed with collected tags + break :rows; }, .rigid => { if (findNamedRowExtensionMonotype(scratches, ext_var, types_store)) |specialized| { try self.appendSpecializedTagUnionTags(specialized, scratches); } - break :rows; // Rigid tag union — treat as closed with collected tags + break :rows; }, .err => { if (std.debug.runtime_safety) { - std.debug.panic( - "Monotype.fromTypeVar(tag_union): error row extension tail", - .{}, - ); + std.debug.panic("Monotype.fromTypeVar(tag_union): error row extension tail", .{}); } unreachable; }, @@ -720,93 +877,75 @@ pub const Store = struct { } const collected_tags = scratches.tags.sliceFromStart(tags_top); - // Sort tags alphabetically to match discriminant assignment order. - // Each ext-chain row is pre-sorted, but the concatenation may not be. - if (scratches.ident_store) |ident_store| { - std.mem.sort(Tag, collected_tags, ident_store, Tag.sortByNameAsc); - } - const tag_span = try self.addTags(allocator, collected_tags); - return try self.addMonotype(allocator, .{ .tag_union = .{ .tags = tag_span } }); + std.mem.sort(TagKey, collected_tags, &self.name_pool, TagKey.sortByNameAsc); + return try self.internTagUnion(collected_tags); }, - .fn_pure => |func| try self.fromFuncType(allocator, types_store, func, false, common_idents, specializations, nominal_cycle_breakers, scratches), - .fn_effectful => |func| try self.fromFuncType(allocator, types_store, func, true, common_idents, specializations, nominal_cycle_breakers, scratches), - .fn_unbound => |func| try self.fromFuncType(allocator, types_store, func, false, common_idents, specializations, nominal_cycle_breakers, scratches), + .fn_pure => |func| try self.fromFuncType(types_store, func, false, common_idents, specializations, nominal_cycle_breakers, scratches), + .fn_effectful => |func| try self.fromFuncType(types_store, func, true, common_idents, specializations, nominal_cycle_breakers, scratches), + .fn_unbound => |func| try self.fromFuncType(types_store, func, false, common_idents, specializations, nominal_cycle_breakers, scratches), }; } fn fromFuncType( - self: *Store, - allocator: Allocator, + self: *TypeInterner, types_store: *const types.Store, func: types.Func, effectful: bool, common_idents: CommonIdents, - specializations: *const std.AutoHashMap(types.Var, Idx), - nominal_cycle_breakers: *std.AutoHashMap(types.Var, Idx), + specializations: *const std.AutoHashMap(types.Var, TypeId), + nominal_cycle_breakers: *std.AutoHashMap(types.Var, TypeId), scratches: *Scratches, - ) Allocator.Error!Idx { + ) Allocator.Error!TypeId { const arg_vars = types_store.sliceVars(func.args); const scratch_top = scratches.idxs.top(); defer scratches.idxs.clearFrom(scratch_top); for (arg_vars) |arg_var| { - const arg_type = try self.fromTypeVar(allocator, types_store, arg_var, common_idents, specializations, nominal_cycle_breakers, scratches); + const arg_type = try self.fromTypeVar(self.allocator, types_store, arg_var, common_idents, specializations, nominal_cycle_breakers, scratches); try scratches.idxs.append(arg_type); } - const args_span = try self.addIdxSpan(allocator, scratches.idxs.sliceFromStart(scratch_top)); - const ret = try self.fromTypeVar(allocator, types_store, func.ret, common_idents, specializations, nominal_cycle_breakers, scratches); - - return try self.addMonotype(allocator, .{ .func = .{ - .args = args_span, - .ret = ret, - .effectful = effectful, - } }); + const ret = try self.fromTypeVar(self.allocator, types_store, func.ret, common_idents, specializations, nominal_cycle_breakers, scratches); + return try self.internFunc(scratches.idxs.sliceFromStart(scratch_top), ret, effectful); } fn fromNominalType( - self: *Store, - allocator: Allocator, + self: *TypeInterner, types_store: *const types.Store, nominal_var: types.Var, nominal: types.NominalType, common_idents: CommonIdents, - specializations: *const std.AutoHashMap(types.Var, Idx), - nominal_cycle_breakers: *std.AutoHashMap(types.Var, Idx), + specializations: *const std.AutoHashMap(types.Var, TypeId), + nominal_cycle_breakers: *std.AutoHashMap(types.Var, TypeId), scratches: *Scratches, - ) Allocator.Error!Idx { + ) Allocator.Error!TypeId { const ident = nominal.ident.ident_idx; const origin = nominal.origin_module; if (origin.eql(common_idents.builtin_module)) { - // Bool/Str: unqualified idents from source declarations if (ident.eql(common_idents.str)) return self.primIdx(.str); - if (ident.eql(common_idents.bool)) return try self.addBoolTagUnion(allocator, common_idents); + if (ident.eql(common_idents.bool)) return try self.addBoolTagUnion(); } if (origin.eql(common_idents.builtin_module)) { - - // List: unqualified ident from mkListContent if (ident.eql(common_idents.list)) { const type_args = types_store.sliceNominalArgs(nominal); if (type_args.len > 0) { - const elem_type = try self.fromTypeVar(allocator, types_store, type_args[0], common_idents, specializations, nominal_cycle_breakers, scratches); - return try self.addMonotype(allocator, .{ .list = .{ .elem = elem_type } }); + const elem_type = try self.fromTypeVar(self.allocator, types_store, type_args[0], common_idents, specializations, nominal_cycle_breakers, scratches); + return try self.internList(elem_type); } return self.unit_idx; } - // Box: unqualified ident from mkBoxContent if (ident.eql(common_idents.box)) { const type_args = types_store.sliceNominalArgs(nominal); if (type_args.len > 0) { - const inner_type = try self.fromTypeVar(allocator, types_store, type_args[0], common_idents, specializations, nominal_cycle_breakers, scratches); - return try self.addMonotype(allocator, .{ .box = .{ .inner = inner_type } }); + const inner_type = try self.fromTypeVar(self.allocator, types_store, type_args[0], common_idents, specializations, nominal_cycle_breakers, scratches); + return try self.internBox(inner_type); } return self.unit_idx; } - // Numeric types: qualified idents from mkNumberTypeContent (e.g. "Builtin.Num.I64") if (ident.eql(common_idents.i64_type)) return self.primIdx(.i64); if (ident.eql(common_idents.u8_type)) return self.primIdx(.u8); if (ident.eql(common_idents.i8_type)) return self.primIdx(.i8); @@ -822,25 +961,17 @@ pub const Store = struct { if (ident.eql(common_idents.dec_type)) return self.primIdx(.dec); } - // For all other nominal types, strip the wrapper and follow the backing var. - // In MIR there is no nominal/opaque/structural distinction. - // - // Recursive nominal types (e.g. Tree := [Leaf, Node(Tree)]) are the - // only legitimate source of type cycles — the type checker has already - // converted infinite/anonymous recursive types to errors. We use - // `nominal_cycle_breakers` (separate from the specialization map) to - // break these cycles: register a placeholder before recursing, then - // overwrite it in-place with the real value. + // Non-builtin nominal: strip wrapper, follow backing var. + // Use cycle breakers for recursive nominal types. if (nominal_cycle_breakers.get(nominal_var)) |cached| return cached; if (findEquivalentNominalCycleBreaker(types_store, nominal, nominal_cycle_breakers)) |cached| { return cached; } - const placeholder_idx = try self.addMonotype(allocator, .recursive_placeholder); - try nominal_cycle_breakers.put(nominal_var, placeholder_idx); + const rec_id = try self.reserveRecursive(); + try nominal_cycle_breakers.put(nominal_var, rec_id); const named_specializations_top = try self.pushNominalArgSpecializations( - allocator, types_store, nominal, common_idents, @@ -851,20 +982,17 @@ pub const Store = struct { defer scratches.named_specializations.clearFrom(named_specializations_top); const backing_var = types_store.getNominalBackingVar(nominal); - const backing_idx = try self.fromTypeVar(allocator, types_store, backing_var, common_idents, specializations, nominal_cycle_breakers, scratches); + const backing_idx = try self.fromTypeVar(self.allocator, types_store, backing_var, common_idents, specializations, nominal_cycle_breakers, scratches); + + self.finalizeRecursive(rec_id, backing_idx); - // Copy the resolved backing type's value into our placeholder slot. - // This value-copy is safe because every field inside a Monotype is an - // index (Idx, Span, Ident.Idx) — never a pointer. The monotype store - // is append-only, so all indices remain valid after the copy. - self.monotypes.items[@intFromEnum(placeholder_idx)] = self.monotypes.items[@intFromEnum(backing_idx)]; if (std.debug.runtime_safety) { - std.debug.assert(self.monotypes.items[@intFromEnum(placeholder_idx)] != .recursive_placeholder); + std.debug.assert(!backing_idx.isNone()); } - return placeholder_idx; + return rec_id; } - fn lookupNamedSpecialization(scratches: *const Scratches, name: Ident.Idx) ?Idx { + fn lookupNamedSpecialization(scratches: *const Scratches, name: Ident.Idx) ?TypeId { const ident_store = scratches.ident_store orelse return null; const name_text = ident_store.getText(name); const items = scratches.named_specializations.items.items; @@ -879,7 +1007,7 @@ pub const Store = struct { return null; } - fn findNamedRowExtensionMonotype(scratches: *const Scratches, ext_var: types.Var, types_store: *const types.Store) ?Idx { + fn findNamedRowExtensionMonotype(scratches: *const Scratches, ext_var: types.Var, types_store: *const types.Store) ?TypeId { const resolved = types_store.resolveVar(ext_var); return switch (resolved.desc.content) { .flex => |flex| if (flex.name) |name| lookupNamedSpecialization(scratches, name) else null, @@ -889,71 +1017,62 @@ pub const Store = struct { } fn appendSpecializedRecordFields( - self: *Store, - specialized: Idx, + self: *TypeInterner, + specialized: TypeId, scratch_top: u32, scratches: *Scratches, ) Allocator.Error!void { - const mono = self.getMonotype(specialized); - switch (mono) { - .record => |record| { - for (self.getFields(record.fields)) |field| { - var seen_name = false; - for (scratches.fields.sliceFromStart(scratch_top)) |existing| { - if (existing.name.eql(field.name)) { - seen_name = true; - break; - } - } - if (seen_name) continue; - try scratches.fields.append(field); - } - }, - .unit => {}, - else => { - if (std.debug.runtime_safety) { - std.debug.panic( - "Monotype.fromTypeVar(record): nominal row specialization must be record or unit, found '{s}'", - .{@tagName(mono)}, - ); + const resolved = self.resolve(specialized); + if (resolved.isUnit()) return; + if (resolved.kind != .record) { + if (std.debug.runtime_safety) { + std.debug.panic( + "Monotype.fromTypeVar(record): nominal row specialization must be record or unit, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } + unreachable; + } + for (self.recordFields(resolved)) |field| { + var seen_name = false; + for (scratches.fields.sliceFromStart(scratch_top)) |existing| { + if (@intFromEnum(existing.name) == @intFromEnum(field.name)) { + seen_name = true; + break; } - unreachable; - }, + } + if (seen_name) continue; + try scratches.fields.append(field); } } fn appendSpecializedTagUnionTags( - self: *Store, - specialized: Idx, + self: *TypeInterner, + specialized: TypeId, scratches: *Scratches, ) Allocator.Error!void { - const mono = self.getMonotype(specialized); - switch (mono) { - .tag_union => |tag_union| { - for (self.getTags(tag_union.tags)) |tag| { - try scratches.tags.append(tag); - } - }, - else => { - if (std.debug.runtime_safety) { - std.debug.panic( - "Monotype.fromTypeVar(tag_union): nominal row specialization must be tag_union, found '{s}'", - .{@tagName(mono)}, - ); - } - unreachable; - }, + const resolved = self.resolve(specialized); + if (resolved.kind != .tag_union) { + if (std.debug.runtime_safety) { + std.debug.panic( + "Monotype.fromTypeVar(tag_union): nominal row specialization must be tag_union, found kind={d}", + .{@intFromEnum(resolved.kind)}, + ); + } + unreachable; + } + for (self.tagUnionTags(resolved)) |tag| { + try scratches.tags.append(tag); } } fn pushNominalArgSpecializations( - self: *Store, - allocator: Allocator, + self: *TypeInterner, types_store: *const types.Store, nominal: types.NominalType, common_idents: CommonIdents, - specializations: *const std.AutoHashMap(types.Var, Idx), - nominal_cycle_breakers: *std.AutoHashMap(types.Var, Idx), + specializations: *const std.AutoHashMap(types.Var, TypeId), + nominal_cycle_breakers: *std.AutoHashMap(types.Var, TypeId), scratches: *Scratches, ) Allocator.Error!u32 { const top = scratches.named_specializations.top(); @@ -979,7 +1098,7 @@ pub const Store = struct { for (formal_args, actual_args) |formal_arg, actual_arg| { const formal_name_text = resolvedTypeVarNameText(&definition_env.types, definition_env, formal_arg) orelse continue; const actual_mono = try self.fromTypeVar( - allocator, + self.allocator, types_store, actual_arg, common_idents, @@ -1045,8 +1164,8 @@ pub const Store = struct { fn findEquivalentNominalCycleBreaker( types_store: *const types.Store, nominal: types.NominalType, - nominal_cycle_breakers: *const std.AutoHashMap(types.Var, Idx), -) ?Idx { + nominal_cycle_breakers: *const std.AutoHashMap(types.Var, TypeId), +) ?TypeId { var iter = nominal_cycle_breakers.iterator(); while (iter.next()) |entry| { const resolved = types_store.resolveVar(entry.key_ptr.*); @@ -1077,3 +1196,6 @@ fn findEquivalentNominalCycleBreaker( return null; } + +/// Backward-compat alias. +pub const Store = TypeInterner; diff --git a/src/mir/test/lower_test.zig b/src/mir/test/lower_test.zig index 9a92adb601c..8fd4edb028b 100644 --- a/src/mir/test/lower_test.zig +++ b/src/mir/test/lower_test.zig @@ -220,15 +220,15 @@ test "Monotype Store: primitive types" { var store = try Monotype.Store.init(test_allocator); defer store.deinit(test_allocator); - try testing.expectEqual(Monotype.Prim.str, store.getMonotype(store.primIdx(.str)).prim); - try testing.expectEqual(Monotype.Prim.i64, store.getMonotype(store.primIdx(.i64)).prim); + try testing.expectEqual(Monotype.Prim.str, store.primIdx(.str).builtinPrim().?); + try testing.expectEqual(Monotype.Prim.i64, store.primIdx(.i64).builtinPrim().?); } test "Monotype Store: unit type" { var store = try Monotype.Store.init(test_allocator); defer store.deinit(test_allocator); - try testing.expect(store.getMonotype(store.unit_idx) == .unit); + try testing.expect(store.unit_idx.isUnit()); } test "Monotype Store: list type" { @@ -236,10 +236,10 @@ test "Monotype Store: list type" { defer store.deinit(test_allocator); const elem = store.primIdx(.str); - const list = try store.addMonotype(test_allocator, .{ .list = .{ .elem = elem } }); + const list = try store.internList(elem); - const retrieved = store.getMonotype(list); - try testing.expectEqual(elem, retrieved.list.elem); + try testing.expect(list.kind == .list); + try testing.expect(store.listElem(list).eql(elem)); } test "Monotype Store: func type" { @@ -250,16 +250,11 @@ test "Monotype Store: func type" { const arg2 = store.primIdx(.str); const ret = store.primIdx(.i64); - const args_span = try store.addIdxSpan(test_allocator, &.{ arg1, arg2 }); - const func = try store.addMonotype(test_allocator, .{ .func = .{ - .args = args_span, - .ret = ret, - .effectful = false, - } }); + const func = try store.internFunc(&.{ arg1, arg2 }, ret, false); - const retrieved = store.getMonotype(func); - try testing.expectEqual(ret, retrieved.func.ret); - try testing.expectEqual(false, retrieved.func.effectful); + try testing.expect(func.kind == .func); + try testing.expect(store.funcRet(func).eql(ret)); + try testing.expect(!store.funcEffectful(func)); } test "Monotype Store: record type" { @@ -269,14 +264,14 @@ test "Monotype Store: record type" { const field1_type = store.primIdx(.i64); const field2_type = store.primIdx(.str); - const field_span = try store.addFields(test_allocator, &.{ - .{ .name = Ident.Idx.NONE, .type_idx = field1_type }, - .{ .name = Ident.Idx.NONE, .type_idx = field2_type }, + const name1 = try store.name_pool.intern("field1"); + const name2 = try store.name_pool.intern("field2"); + const record = try store.internRecord(&.{ + .{ .name = name1, .ty = field1_type }, + .{ .name = name2, .ty = field2_type }, }); - const record = try store.addMonotype(test_allocator, .{ .record = .{ .fields = field_span } }); - const retrieved = store.getMonotype(record); - try testing.expect(retrieved == .record); + try testing.expect(record.kind == .record); } test "Monotype Store: tag union type" { @@ -284,15 +279,13 @@ test "Monotype Store: tag union type" { defer store.deinit(test_allocator); const payload_type = store.primIdx(.str); - const payload_span = try store.addIdxSpan(test_allocator, &.{payload_type}); - - const tag_span = try store.addTags(test_allocator, &.{ - .{ .name = Ident.Idx.NONE, .payloads = payload_span }, + const tag_name = try store.name_pool.intern("MyTag"); + const payload_list = try store.internIdxList(&.{payload_type}); + const tag_union = try store.internTagUnion(&.{ + .{ .name = tag_name, .payloads = payload_list }, }); - const tag_union = try store.addMonotype(test_allocator, .{ .tag_union = .{ .tags = tag_span } }); - const retrieved = store.getMonotype(tag_union); - try testing.expect(retrieved == .tag_union); + try testing.expect(tag_union.kind == .tag_union); } test "Monotype Store: box type" { @@ -300,10 +293,10 @@ test "Monotype Store: box type" { defer store.deinit(test_allocator); const inner = store.primIdx(.i64); - const boxed = try store.addMonotype(test_allocator, .{ .box = .{ .inner = inner } }); + const boxed = try store.internBox(inner); - const retrieved = store.getMonotype(boxed); - try testing.expectEqual(inner, retrieved.box.inner); + try testing.expect(boxed.kind == .box); + try testing.expect(store.boxInner(boxed).eql(inner)); } test "Monotype Store: tuple type" { @@ -313,11 +306,9 @@ test "Monotype Store: tuple type" { const elem1 = store.primIdx(.i64); const elem2 = store.primIdx(.str); - const elems_span = try store.addIdxSpan(test_allocator, &.{ elem1, elem2 }); - const tuple = try store.addMonotype(test_allocator, .{ .tuple = .{ .elems = elems_span } }); + const tuple = try store.internTuple(&.{ elem1, elem2 }); - const retrieved = store.getMonotype(tuple); - try testing.expect(retrieved == .tuple); + try testing.expect(tuple.kind == .tuple); } test "Monotype Store: all primitive types" { @@ -343,7 +334,7 @@ test "Monotype Store: all primitive types" { for (prims) |p| { const idx = store.primIdx(p); - try testing.expectEqual(p, store.getMonotype(idx).prim); + try testing.expectEqual(p, idx.builtinPrim().?); } } @@ -1039,9 +1030,8 @@ test "lambda set: higher-order closure captures monotype stays numeric tuple" { const closure_member = env.mir_store.getClosureMember(members[0].closure_member); const capture_bindings = env.mir_store.getCaptureBindings(closure_member.capture_bindings); try testing.expectEqual(@as(usize, 1), capture_bindings.len); - const elem_mono = env.mir_store.monotype_store.getMonotype(capture_bindings[0].monotype); - try testing.expect(elem_mono == .prim); - try testing.expectEqual(Monotype.Prim.dec, elem_mono.prim); + const elem_mono = env.mir_store.monotype_store.resolve(capture_bindings[0].monotype); + try testing.expectEqual(Monotype.Prim.dec, elem_mono.builtinPrim().?); } test "lambda set: imported List.any receives predicate lambda set" { @@ -1080,21 +1070,18 @@ test "lambda set: imported List.any receives predicate lambda set" { const param_ids = env.mir_store.getPatternSpan(params); try testing.expectEqual(@as(usize, 2), param_ids.len); - const list_param_mono = env.mir_store.monotype_store.getMonotype(env.mir_store.patternTypeOf(param_ids[0])); - try testing.expect(list_param_mono == .list); - const list_elem_mono = env.mir_store.monotype_store.getMonotype(list_param_mono.list.elem); - try testing.expect(list_elem_mono == .prim); - try testing.expectEqual(Monotype.Prim.i64, list_elem_mono.prim); + const list_param_id = env.mir_store.monotype_store.resolve(env.mir_store.patternTypeOf(param_ids[0])); + try testing.expect(list_param_id.kind == .list); + const list_elem_id = env.mir_store.monotype_store.listElem(list_param_id); + try testing.expectEqual(Monotype.Prim.i64, list_elem_id.builtinPrim().?); - const predicate_param_mono = env.mir_store.monotype_store.getMonotype(env.mir_store.patternTypeOf(param_ids[1])); - try testing.expect(predicate_param_mono == .func); - const predicate_arg_monos = env.mir_store.monotype_store.getIdxSpan(predicate_param_mono.func.args); + const predicate_param_id = env.mir_store.monotype_store.resolve(env.mir_store.patternTypeOf(param_ids[1])); + try testing.expect(predicate_param_id.kind == .func); + const predicate_arg_monos = env.mir_store.monotype_store.funcArgs(predicate_param_id); try testing.expectEqual(@as(usize, 1), predicate_arg_monos.len); - const predicate_arg_mono = env.mir_store.monotype_store.getMonotype(predicate_arg_monos[0]); - try testing.expect(predicate_arg_mono == .prim); - try testing.expectEqual(Monotype.Prim.i64, predicate_arg_mono.prim); - const predicate_ret_mono = env.mir_store.monotype_store.getMonotype(predicate_param_mono.func.ret); - try testing.expect(predicate_ret_mono == .tag_union); + try testing.expectEqual(Monotype.Prim.i64, predicate_arg_monos[0].builtinPrim().?); + const predicate_ret_id = env.mir_store.monotype_store.funcRet(predicate_param_id); + try testing.expect(predicate_ret_id.kind == .tag_union); const predicate_pat = env.mir_store.getPattern(param_ids[1]); try testing.expect(predicate_pat == .bind); @@ -1128,9 +1115,8 @@ test "lambda set: imported List.any receives predicate lambda set" { } } - const loop_item_mono = env.mir_store.monotype_store.getMonotype(env.mir_store.patternTypeOf(loop_item_pat.?)); - try testing.expect(loop_item_mono == .prim); - try testing.expectEqual(Monotype.Prim.i64, loop_item_mono.prim); + const loop_item_id = env.mir_store.monotype_store.resolve(env.mir_store.patternTypeOf(loop_item_pat.?)); + try testing.expectEqual(Monotype.Prim.i64, loop_item_id.builtinPrim().?); const all_module_envs = [_]*ModuleEnv{ @constCast(env.builtin_module.env), @@ -1185,9 +1171,8 @@ test "fromTypeVar: int with suffix resolves to prim i64" { var env = try MirTestEnv.initExpr("42.I64"); defer env.deinit(); const expr = try env.lowerFirstDef(); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .prim); - try testing.expectEqual(Monotype.Prim.i64, monotype.prim); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expectEqual(Monotype.Prim.i64, mono_id.builtinPrim().?); } test "fromTypeVar: string resolves to valid monotype" { @@ -1215,17 +1200,17 @@ test "fromTypeVar: list resolves to list monotype" { // This catches the cross-module ident mismatch bug where // fromNominalType fails to recognize List because the Builtin // module's Ident.Idx for "List" differs from the current module's. - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .list); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .list); } test "fromTypeVar: record resolves to record with fields" { var env = try MirTestEnv.initExpr("{ x: 1, y: 2 }"); defer env.deinit(); const expr = try env.lowerFirstDef(); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .record); - try testing.expectEqual(@as(u16, 2), monotype.record.fields.len); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .record); + try testing.expectEqual(@as(usize, 2), env.mir_store.monotype_store.recordFields(mono_id).len); } test "fromTypeVar: lambda resolves to func type" { @@ -1235,17 +1220,17 @@ test "fromTypeVar: lambda resolves to func type" { ); defer env.deinit(); const expr = try env.lowerFirstDef(); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .func); - try testing.expectEqual(@as(u16, 1), monotype.func.args.len); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .func); + try testing.expectEqual(@as(usize, 1), env.mir_store.monotype_store.funcArgs(mono_id).len); } test "fromTypeVar: tag resolves to tag_union" { var env = try MirTestEnv.initExpr("Ok(42)"); defer env.deinit(); const expr = try env.lowerFirstDef(); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "fromTypeVar: tuple resolves to tuple type" { @@ -1254,9 +1239,9 @@ test "fromTypeVar: tuple resolves to tuple type" { ); defer env.deinit(); const expr = try env.lowerFirstDef(); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tuple); - try testing.expectEqual(@as(u16, 2), monotype.tuple.elems.len); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tuple); + try testing.expectEqual(@as(usize, 2), env.mir_store.monotype_store.tupleElems(mono_id).len); } // --- Gap #25: Recursive types in fromTypeVar --- @@ -1272,8 +1257,8 @@ test "fromTypeVar: recursive linked list type completes without hanging" { const expr = try env.lowerNamedDef("x"); const result = env.mir_store.getExpr(expr); try testing.expect(result == .tag); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "fromTypeVar: recursive binary tree type completes without hanging" { @@ -1287,8 +1272,8 @@ test "fromTypeVar: recursive binary tree type completes without hanging" { const expr = try env.lowerNamedDef("x"); const result = env.mir_store.getExpr(expr); try testing.expect(result == .tag); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "fromTypeVar: polymorphic opaque with function field propagates nominal args into backing type" { @@ -1304,27 +1289,25 @@ test "fromTypeVar: polymorphic opaque with function field propagates nominal arg defer env.deinit(); const expr = try env.lowerNamedDef("w"); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .record); + try testing.expect(mono_id.kind == .record); - const fields = env.mir_store.monotype_store.getFields(monotype.record.fields); + const fields = env.mir_store.monotype_store.recordFields(mono_id); try testing.expectEqual(@as(usize, 1), fields.len); - const field_type = env.mir_store.monotype_store.getMonotype(fields[0].type_idx); - try testing.expect(field_type == .func); + const field_type = env.mir_store.monotype_store.resolve(fields[0].ty); + try testing.expect(field_type.kind == .func); - const ret_type = env.mir_store.monotype_store.getMonotype(field_type.func.ret); - try testing.expect(ret_type == .tag_union); + const ret_type = env.mir_store.monotype_store.resolve(env.mir_store.monotype_store.funcRet(field_type)); + try testing.expect(ret_type.kind == .tag_union); - const tags = env.mir_store.monotype_store.getTags(ret_type.tag_union.tags); + const tags = env.mir_store.monotype_store.tagUnionTags(ret_type); try testing.expectEqual(@as(usize, 1), tags.len); - try testing.expectEqual(@as(usize, 1), tags[0].payloads.len); - const payloads = env.mir_store.monotype_store.getIdxSpan(tags[0].payloads); - const payload_type = env.mir_store.monotype_store.getMonotype(payloads[0]); - try testing.expect(payload_type == .prim); - try testing.expectEqual(Monotype.Prim.str, payload_type.prim); + const payloads = env.mir_store.monotype_store.getIdxListItems(tags[0].payloads); + try testing.expectEqual(@as(usize, 1), payloads.len); + try testing.expectEqual(Monotype.Prim.str, payloads[0].builtinPrim().?); } test "lambda set: opaque function field call through param gets field lambda set" { @@ -1776,10 +1759,10 @@ test "lowerExternalDef: mutually recursive defs get monotypes patched (not left try testing.expect(odd_type != env.mir_store.monotype_store.unit_idx); // Both should resolve to func types (U64 -> Bool) - const even_mono = env.mir_store.monotype_store.getMonotype(even_type); - const odd_mono = env.mir_store.monotype_store.getMonotype(odd_type); - try testing.expect(even_mono == .func); - try testing.expect(odd_mono == .func); + const even_resolved = env.mir_store.monotype_store.resolve(even_type); + const odd_resolved = env.mir_store.monotype_store.resolve(odd_type); + try testing.expect(even_resolved.kind == .func); + try testing.expect(odd_resolved.kind == .func); } // --- Cross-module builtin call lowering --- @@ -2222,8 +2205,8 @@ test "Bool diagnostic MIR: Bool.True lowers to tag with tag_union" { const tag_name = env.module_env.getIdent(result.tag.name); try testing.expectEqualStrings("True", tag_name); // Monotype should be tag_union - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "Bool diagnostic MIR: Bool.False lowers to tag with tag_union" { @@ -2235,8 +2218,8 @@ test "Bool diagnostic MIR: Bool.False lowers to tag with tag_union" { try testing.expectEqual(@as(u16, 0), result.tag.args.len); const tag_name = env.module_env.getIdent(result.tag.name); try testing.expectEqualStrings("False", tag_name); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "Bool diagnostic MIR: Bool.not(True) lowers with tag_union type" { @@ -2246,8 +2229,8 @@ test "Bool diagnostic MIR: Bool.not(True) lowers with tag_union type" { const result = env.mir_store.getExpr(expr); // Bool.not may lower as a cross-module call or inlined match try testing.expect(result != .runtime_err_type); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "Bool diagnostic MIR: Bool.not(False) lowers with tag_union type" { @@ -2256,8 +2239,8 @@ test "Bool diagnostic MIR: Bool.not(False) lowers with tag_union type" { const expr = try env.lowerFirstDef(); const result = env.mir_store.getExpr(expr); try testing.expect(result != .runtime_err_type); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "Bool diagnostic MIR: !Bool.True lowers to match_expr with tag_union" { @@ -2269,8 +2252,8 @@ test "Bool diagnostic MIR: !Bool.True lowers to match_expr with tag_union" { const expr = try env.lowerFirstDef(); // !Bool.True desugars via negBool to a match expression try testing.expect(env.mir_store.getExpr(expr) == .match_expr); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "Bool diagnostic MIR: !Bool.False lowers to match_expr with tag_union" { @@ -2281,8 +2264,8 @@ test "Bool diagnostic MIR: !Bool.False lowers to match_expr with tag_union" { defer env.deinit(); const expr = try env.lowerFirstDef(); try testing.expect(env.mir_store.getExpr(expr) == .match_expr); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "Bool diagnostic MIR: Bool.True and Bool.False lowers to match_expr with tag_union" { @@ -2292,8 +2275,8 @@ test "Bool diagnostic MIR: Bool.True and Bool.False lowers to match_expr with ta const result = env.mir_store.getExpr(expr); // `and` short-circuit desugars to a match expression try testing.expect(result == .match_expr); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "Bool diagnostic MIR: !Bool.True or !Bool.True lowers with tag_union" { @@ -2306,8 +2289,8 @@ test "Bool diagnostic MIR: !Bool.True or !Bool.True lowers with tag_union" { const result = env.mir_store.getExpr(expr); // `or` short-circuit desugars to a match expression try testing.expect(result == .match_expr); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "Bool diagnostic MIR: lambda negation applied to Bool.True lowers with tag_union" { @@ -2317,8 +2300,8 @@ test "Bool diagnostic MIR: lambda negation applied to Bool.True lowers with tag_ const result = env.mir_store.getExpr(expr); // A lambda call should produce a .call expression try testing.expect(result != .runtime_err_type); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } // --- Bool.not structural tests --- @@ -2342,8 +2325,8 @@ test "Bool.not MIR: !Bool.True match has True pattern -> False body, wildcard -> try testing.expectEqualStrings("True", cond_tag_name); // Check condition monotype is tag_union - const cond_mono = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(result.match_expr.cond)); - try testing.expect(cond_mono == .tag_union); + const cond_mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(result.match_expr.cond)); + try testing.expect(cond_mono_id.kind == .tag_union); // Check there are 2 branches const branches = env.mir_store.getBranches(result.match_expr.branches); @@ -2357,8 +2340,8 @@ test "Bool.not MIR: !Bool.True match has True pattern -> False body, wildcard -> const pat0_name = env.module_env.getIdent(pat0.tag.name); try testing.expectEqualStrings("True", pat0_name); // Pattern monotype should be tag_union - const pat0_mono = env.mir_store.monotype_store.getMonotype(env.mir_store.patternTypeOf(bp0[0].pattern)); - try testing.expect(pat0_mono == .tag_union); + const pat0_mono_id = env.mir_store.monotype_store.resolve(env.mir_store.patternTypeOf(bp0[0].pattern)); + try testing.expect(pat0_mono_id.kind == .tag_union); // Body should be False tag const body0 = env.mir_store.getExpr(branches[0].body); try testing.expect(body0 == .tag); @@ -2392,8 +2375,8 @@ test "Bool.not MIR: !Bool.False match has True pattern -> False body, wildcard - try testing.expect(cond == .tag); const cond_tag_name = env.module_env.getIdent(cond.tag.name); try testing.expectEqualStrings("False", cond_tag_name); - const cond_mono = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(result.match_expr.cond)); - try testing.expect(cond_mono == .tag_union); + const cond_mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(result.match_expr.cond)); + try testing.expect(cond_mono_id.kind == .tag_union); // Branch structure should be identical: True => False, _ => True const branches = env.mir_store.getBranches(result.match_expr.branches); @@ -2423,16 +2406,16 @@ test "Bool.not MIR: Bool.not(True) is a call with tag_union return type" { // Bool.not(True) should be a call expression (cross-module method call) try testing.expect(result == .call); // Return type should be tag_union - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); // The argument should be a tag (True) with tag_union type const args = env.mir_store.getExprSpan(result.call.args); try testing.expectEqual(@as(usize, 1), args.len); const arg = env.mir_store.getExpr(args[0]); try testing.expect(arg == .tag); try testing.expectEqualStrings("True", env.module_env.getIdent(arg.tag.name)); - const arg_mono = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(args[0])); - try testing.expect(arg_mono == .tag_union); + const arg_mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(args[0])); + try testing.expect(arg_mono_id.kind == .tag_union); } test "Bool.not MIR: Bool.not(False) is a call with tag_union return type" { @@ -2441,15 +2424,15 @@ test "Bool.not MIR: Bool.not(False) is a call with tag_union return type" { const expr = try env.lowerFirstDef(); const result = env.mir_store.getExpr(expr); try testing.expect(result == .call); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); const args = env.mir_store.getExprSpan(result.call.args); try testing.expectEqual(@as(usize, 1), args.len); const arg = env.mir_store.getExpr(args[0]); try testing.expect(arg == .tag); try testing.expectEqualStrings("False", env.module_env.getIdent(arg.tag.name)); - const arg_mono = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(args[0])); - try testing.expect(arg_mono == .tag_union); + const arg_mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(args[0])); + try testing.expect(arg_mono_id.kind == .tag_union); } // --- Nominal Bool vs structural tag union MIR tests --- @@ -2464,8 +2447,8 @@ test "Nominal Bool MIR: annotated True lowers with tag_union" { ); defer env.deinit(); const expr = try env.lowerFirstDef(); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "Nominal Bool MIR: annotated False lowers with tag_union" { @@ -2475,32 +2458,32 @@ test "Nominal Bool MIR: annotated False lowers with tag_union" { ); defer env.deinit(); const expr = try env.lowerFirstDef(); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "Structural tag MIR: bare True lowers as tag_union" { var env = try MirTestEnv.initExpr("True"); defer env.deinit(); const expr = try env.lowerFirstDef(); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "Structural tag MIR: bare False lowers as tag_union" { var env = try MirTestEnv.initExpr("False"); defer env.deinit(); const expr = try env.lowerFirstDef(); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "Structural tag MIR: if True True else False lowers as tag_union" { var env = try MirTestEnv.initExpr("if True True else False"); defer env.deinit(); const expr = try env.lowerFirstDef(); - const monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(monotype == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } // --- Cross-module type resolution: method dispatch resolves concrete types --- @@ -2522,29 +2505,27 @@ test "cross-module type resolution: U32.to dispatches with concrete U32 function const func_expr = env.mir_store.getExpr(top.call.func); try testing.expect(func_expr == .lookup); - const func_monotype_idx = env.mir_store.typeOf(top.call.func); - const func_mono = env.mir_store.monotype_store.getMonotype(func_monotype_idx); - try testing.expect(func_mono == .func); + const func_mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(top.call.func)); + try testing.expect(func_mono_id.kind == .func); // The function's return type must be List(U32), not List(unit) - const ret_mono = env.mir_store.monotype_store.getMonotype(func_mono.func.ret); - try testing.expect(ret_mono == .list); + const ret_mono_id = env.mir_store.monotype_store.resolve(env.mir_store.monotype_store.funcRet(func_mono_id)); + try testing.expect(ret_mono_id.kind == .list); - const elem_mono = env.mir_store.monotype_store.getMonotype(ret_mono.list.elem); - try testing.expectEqual(Monotype.Monotype{ .prim = .u32 }, elem_mono); + const elem_mono_id = env.mir_store.monotype_store.listElem(ret_mono_id); + try testing.expectEqual(Monotype.Prim.u32, elem_mono_id.builtinPrim().?); // Verify the function DEFINITION body was also lowered with concrete types. const method_symbol = func_expr.lookup; const sym_key: u64 = @bitCast(method_symbol); const def_expr_id = env.mir_store.symbol_defs.get(sym_key) orelse return error.TestUnexpectedResult; - const def_mono_idx = env.mir_store.typeOf(def_expr_id); - const def_mono = env.mir_store.monotype_store.getMonotype(def_mono_idx); - try testing.expect(def_mono == .func); - const def_ret_mono = env.mir_store.monotype_store.getMonotype(def_mono.func.ret); - try testing.expect(def_ret_mono == .list); - const def_elem = env.mir_store.monotype_store.getMonotype(def_ret_mono.list.elem); - try testing.expectEqual(Monotype.Monotype{ .prim = .u32 }, def_elem); + const def_mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(def_expr_id)); + try testing.expect(def_mono_id.kind == .func); + const def_ret_id = env.mir_store.monotype_store.resolve(env.mir_store.monotype_store.funcRet(def_mono_id)); + try testing.expect(def_ret_id.kind == .list); + const def_elem_id = env.mir_store.monotype_store.listElem(def_ret_id); + try testing.expectEqual(Monotype.Prim.u32, def_elem_id.builtinPrim().?); } // --- Polymorphic numeric specialization tests --- @@ -2573,15 +2554,14 @@ test "polymorphic lambda in block: sum called with U64 gets U64 monotype, not De try testing.expect(final_expr == .call); // The call's return type must be U64, not Dec - const call_monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(result.block.final_expr)); - try testing.expectEqual(Monotype.Prim.u64, call_monotype.prim); + const call_mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(result.block.final_expr)); + try testing.expectEqual(Monotype.Prim.u64, call_mono_id.builtinPrim().?); // The call's arguments must be U64 const args = env.mir_store.getExprSpan(final_expr.call.args); for (args) |arg| { - const arg_mono = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(arg)); - try testing.expect(arg_mono == .prim); - try testing.expectEqual(Monotype.Prim.u64, arg_mono.prim); + const arg_mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(arg)); + try testing.expectEqual(Monotype.Prim.u64, arg_mono_id.builtinPrim().?); } // The lambda itself (first stmt's expr) should have params typed as U64 @@ -2589,12 +2569,11 @@ test "polymorphic lambda in block: sum called with U64 gets U64 monotype, not De try testing.expect(stmts.len >= 1); const decl_expr = env.mir_store.getExpr(stmts[0].decl_const.expr); try testing.expect(decl_expr == .lambda); - const lambda_mono = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(stmts[0].decl_const.expr)); - try testing.expect(lambda_mono == .func); + const lambda_mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(stmts[0].decl_const.expr)); + try testing.expect(lambda_mono_id.kind == .func); // Return type of the lambda must be U64 - const ret_mono = env.mir_store.monotype_store.getMonotype(lambda_mono.func.ret); - try testing.expect(ret_mono == .prim); - try testing.expectEqual(Monotype.Prim.u64, ret_mono.prim); + const ret_mono_id = env.mir_store.monotype_store.funcRet(lambda_mono_id); + try testing.expectEqual(Monotype.Prim.u64, ret_mono_id.builtinPrim().?); } test "polymorphic lambda in block: fn called via arrow syntax gets correct type" { @@ -2611,9 +2590,8 @@ test "polymorphic lambda in block: fn called via arrow syntax gets correct type" try testing.expect(result == .block); // The final expression's return type must be U64 - const call_monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(result.block.final_expr)); - try testing.expect(call_monotype == .prim); - try testing.expectEqual(Monotype.Prim.u64, call_monotype.prim); + const call_mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(result.block.final_expr)); + try testing.expectEqual(Monotype.Prim.u64, call_mono_id.builtinPrim().?); } test "polymorphic lambda with literal in body: a + b + 0 called with U64" { @@ -2629,8 +2607,8 @@ test "polymorphic lambda with literal in body: a + b + 0 called with U64" { try testing.expect(result == .block); // The call's return type must be U64 - const call_monotype = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(result.block.final_expr)); - try testing.expectEqual(Monotype.Prim.u64, call_monotype.prim); + const call_mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(result.block.final_expr)); + try testing.expectEqual(Monotype.Prim.u64, call_mono_id.builtinPrim().?); // Check the lambda body const stmts = env.mir_store.getStmts(result.block.stmts); @@ -2639,17 +2617,17 @@ test "polymorphic lambda with literal in body: a + b + 0 called with U64" { try testing.expect(decl_expr == .lambda); // Lambda return must be U64 - const lambda_mono = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(stmts[0].decl_const.expr)); - try testing.expect(lambda_mono == .func); - const ret_mono = env.mir_store.monotype_store.getMonotype(lambda_mono.func.ret); - try testing.expectEqual(Monotype.Prim.u64, ret_mono.prim); + const lambda_mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(stmts[0].decl_const.expr)); + try testing.expect(lambda_mono_id.kind == .func); + const ret_mono_id = env.mir_store.monotype_store.funcRet(lambda_mono_id); + try testing.expectEqual(Monotype.Prim.u64, ret_mono_id.builtinPrim().?); // Check that the lambda body's subexpressions are all U64, not Dec // The body is `a + b + 0` which desugars to `(a + b) + 0` // The body is either a call or run_low_level for the outer `+` const body = env.mir_store.getExpr(decl_expr.lambda.body); - const body_mono = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(decl_expr.lambda.body)); - try testing.expectEqual(Monotype.Prim.u64, body_mono.prim); + const body_mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(decl_expr.lambda.body)); + try testing.expectEqual(Monotype.Prim.u64, body_mono_id.builtinPrim().?); // The outer `+` has args: (a + b) and 0 // Check that the 0 literal has U64 monotype @@ -2658,8 +2636,8 @@ test "polymorphic lambda with literal in body: a + b + 0 called with U64" { if (ll_args.len == 2) { const zero_expr = env.mir_store.getExpr(ll_args[1]); try testing.expect(zero_expr == .int); - const zero_mono = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(ll_args[1])); - try testing.expectEqual(Monotype.Prim.u64, zero_mono.prim); + const zero_mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(ll_args[1])); + try testing.expectEqual(Monotype.Prim.u64, zero_mono_id.builtinPrim().?); } } else if (body == .call) { const call_args = env.mir_store.getExprSpan(body.call.args); @@ -2687,8 +2665,8 @@ test "structural equality: record == produces match_expr (field-by-field)" { try testing.expectEqual(@as(usize, 2), bindings.len); try testing.expect(env.mir_store.getExpr(result.borrow_scope.body) == .match_expr); // Return type should be Bool - const mono = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(mono == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "structural equality: record != produces negated match_expr" { @@ -2701,8 +2679,8 @@ test "structural equality: record != produces negated match_expr" { const result = env.mir_store.getExpr(expr); // != wraps the == result in negBool (match True => False, _ => True) try testing.expect(result == .match_expr); - const mono = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(mono == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "structural equality: empty record == is True" { @@ -2730,8 +2708,8 @@ test "structural equality: tuple == produces match_expr" { const bindings = env.mir_store.getBorrowBindings(result.borrow_scope.bindings); try testing.expectEqual(@as(usize, 2), bindings.len); try testing.expect(env.mir_store.getExpr(result.borrow_scope.body) == .match_expr); - const mono = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(mono == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "structural equality: tag union == produces nested match_expr" { @@ -2748,8 +2726,8 @@ test "structural equality: tag union == produces nested match_expr" { const bindings = env.mir_store.getBorrowBindings(result.borrow_scope.bindings); try testing.expectEqual(@as(usize, 2), bindings.len); try testing.expect(env.mir_store.getExpr(result.borrow_scope.body) == .match_expr); - const mono = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(mono == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "structural equality: single-field record produces run_low_level (no match)" { @@ -2791,8 +2769,8 @@ test "structural equality: list == produces block with length check" { try testing.expect(result == .borrow_scope); const bindings = env.mir_store.getBorrowBindings(result.borrow_scope.bindings); try testing.expectEqual(@as(usize, 2), bindings.len); - const mono = env.mir_store.monotype_store.getMonotype(env.mir_store.typeOf(expr)); - try testing.expect(mono == .tag_union); + const mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(expr)); + try testing.expect(mono_id.kind == .tag_union); } test "Dec.abs lowers to num_abs with Dec monotype, not unit" { @@ -2803,15 +2781,14 @@ test "Dec.abs lowers to num_abs with Dec monotype, not unit" { // The result monotype must be Dec, not unit const mono_idx = env.mir_store.typeOf(expr); - const mono = env.mir_store.monotype_store.getMonotype(mono_idx); - try testing.expectEqual(Monotype.Monotype{ .prim = .dec }, mono); + const mono_id = env.mir_store.monotype_store.resolve(mono_idx); + try testing.expectEqual(Monotype.Prim.dec, mono_id.builtinPrim().?); // If it's a call, check the function's monotype too if (top == .call) { - const func_mono_idx = env.mir_store.typeOf(top.call.func); - const func_mono = env.mir_store.monotype_store.getMonotype(func_mono_idx); - try testing.expect(func_mono == .func); - const ret = env.mir_store.monotype_store.getMonotype(func_mono.func.ret); - try testing.expectEqual(Monotype.Monotype{ .prim = .dec }, ret); + const func_mono_id = env.mir_store.monotype_store.resolve(env.mir_store.typeOf(top.call.func)); + try testing.expect(func_mono_id.kind == .func); + const ret_id = env.mir_store.monotype_store.funcRet(func_mono_id); + try testing.expectEqual(Monotype.Prim.dec, ret_id.builtinPrim().?); } } From 4ae616145efab78e1eb237b02229db13b762bff1 Mon Sep 17 00:00:00 2001 From: "Luke Boswell (Linux-Desktop)" Date: Thu, 19 Mar 2026 16:23:38 +1100 Subject: [PATCH 3/8] Add nominal instance cache for content-based dedup of nominal types Replace the var-based nominal_cycle_breakers with a canonical NominalInstanceKey cache that deduplicates by (origin, type_name, args). This avoids identity bugs from multiple type vars referring to the same nominal instantiation, and skips unnecessary .rec indirection for non-recursive nominals. Fix ident resolution to fall back to scratches.ident_store when module_env is unavailable (layout tests). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/mir/Monotype.zig | 169 ++++++++++++++++++++++++------------ src/mir/test/lower_test.zig | 35 ++++++++ 2 files changed, 150 insertions(+), 54 deletions(-) diff --git a/src/mir/Monotype.zig b/src/mir/Monotype.zig index 86bb6e418c1..1e2d2c3ffef 100644 --- a/src/mir/Monotype.zig +++ b/src/mir/Monotype.zig @@ -181,6 +181,20 @@ pub const StructuralNamePool = struct { } }; +/// Key identifying a nominal type instance by its origin, type name, and type arguments. +pub const NominalInstanceKey = struct { + origin_name_id: StructuralNameId, + type_name_id: StructuralNameId, + args: IdxListId, +}; + +const NominalCacheEntry = union(enum) { + /// Lowering is in progress. rec_id is allocated lazily on recursive hit. + in_progress: TypeId, // TypeId.none until recursion detected + /// Fully lowered canonical type. + resolved: TypeId, +}; + // ═══════════════════════════════════════════════════════════════ // Internal helpers // ═══════════════════════════════════════════════════════════════ @@ -286,6 +300,9 @@ pub const TypeInterner = struct { // Structural name pool name_pool: StructuralNamePool, + // Nominal instance cache + nominal_cache: std.AutoHashMapUnmanaged(NominalInstanceKey, NominalCacheEntry), + // Pre-interned builtins unit_idx: TypeId, prim_idxs: [prim_count]TypeId, @@ -354,6 +371,7 @@ pub const TypeInterner = struct { .union_map = .{}, .func_map = .{}, .name_pool = StructuralNamePool.init(allocator), + .nominal_cache = .{}, .unit_idx = undefined, .prim_idxs = undefined, .bool_tag_union_idx = TypeId.none, @@ -390,6 +408,7 @@ pub const TypeInterner = struct { self.union_map.deinit(self.allocator); self.func_map.deinit(self.allocator); self.name_pool.deinit(); + self.nominal_cache.deinit(self.allocator); } /// Look up the pre-interned TypeId for a primitive type. @@ -654,7 +673,7 @@ pub const TypeInterner = struct { return try self.fromTypeVar(self.allocator, types_store, backing_var, common_idents, specializations, nominal_cycle_breakers, scratches); }, .structure => |flat_type| { - return try self.fromFlatType(types_store, resolved.var_, flat_type, common_idents, specializations, nominal_cycle_breakers, scratches); + return try self.fromFlatType(types_store, flat_type, common_idents, specializations, nominal_cycle_breakers, scratches); }, .err => return self.unit_idx, }; @@ -663,7 +682,6 @@ pub const TypeInterner = struct { fn fromFlatType( self: *TypeInterner, types_store: *const types.Store, - var_: types.Var, flat_type: types.FlatType, common_idents: CommonIdents, specializations: *const std.AutoHashMap(types.Var, TypeId), @@ -672,7 +690,7 @@ pub const TypeInterner = struct { ) Allocator.Error!TypeId { return switch (flat_type) { .nominal_type => |nominal| { - return try self.fromNominalType(types_store, var_, nominal, common_idents, specializations, nominal_cycle_breakers, scratches); + return try self.fromNominalType(types_store, nominal, common_idents, specializations, nominal_cycle_breakers, scratches); }, .empty_record => self.unit_idx, .empty_tag_union => try self.internTagUnion(&.{}), @@ -909,10 +927,48 @@ pub const TypeInterner = struct { return try self.internFunc(scratches.idxs.sliceFromStart(scratch_top), ret, effectful); } + fn buildNominalCacheKey( + self: *TypeInterner, + types_store: *const types.Store, + nominal: types.NominalType, + common_idents: CommonIdents, + specializations: *const std.AutoHashMap(types.Var, TypeId), + nominal_cycle_breakers: *std.AutoHashMap(types.Var, TypeId), + scratches: *Scratches, + ) Allocator.Error!NominalInstanceKey { + const ident_store = if (scratches.module_env) |env| env.getIdentStoreConst() else scratches.ident_store.?; + const origin_text = ident_store.getText(nominal.origin_module); + const type_text = ident_store.getText(nominal.ident.ident_idx); + const origin_name_id = try self.name_pool.intern(origin_text); + const type_name_id = try self.name_pool.intern(type_text); + + const actual_args = types_store.sliceNominalArgs(nominal); + const idx_top = scratches.idxs.top(); + defer scratches.idxs.clearFrom(idx_top); + for (actual_args) |arg_var| { + const arg_mono = try self.fromTypeVar( + self.allocator, + types_store, + arg_var, + common_idents, + specializations, + nominal_cycle_breakers, + scratches, + ); + try scratches.idxs.append(arg_mono); + } + const args = try self.internIdxList(scratches.idxs.sliceFromStart(idx_top)); + + return .{ + .origin_name_id = origin_name_id, + .type_name_id = type_name_id, + .args = args, + }; + } + fn fromNominalType( self: *TypeInterner, types_store: *const types.Store, - nominal_var: types.Var, nominal: types.NominalType, common_idents: CommonIdents, specializations: *const std.AutoHashMap(types.Var, TypeId), @@ -961,16 +1017,38 @@ pub const TypeInterner = struct { if (ident.eql(common_idents.dec_type)) return self.primIdx(.dec); } - // Non-builtin nominal: strip wrapper, follow backing var. - // Use cycle breakers for recursive nominal types. - if (nominal_cycle_breakers.get(nominal_var)) |cached| return cached; - if (findEquivalentNominalCycleBreaker(types_store, nominal, nominal_cycle_breakers)) |cached| { - return cached; + // Non-builtin nominal: use nominal instance cache for dedup. + // 1. Build canonical cache key (lowers args as side effect). + const cache_key = try self.buildNominalCacheKey( + types_store, + nominal, + common_idents, + specializations, + nominal_cycle_breakers, + scratches, + ); + + // 2. Check nominal instance cache. + if (self.nominal_cache.get(cache_key)) |entry| { + switch (entry) { + .resolved => |type_id| return type_id, + .in_progress => |rec_id| { + // Recursive hit: allocate .rec lazily if first time. + if (rec_id.isNone()) { + const new_rec = try self.reserveRecursive(); + // Must re-fetch after potential map growth from reserveRecursive. + self.nominal_cache.getPtr(cache_key).?.* = .{ .in_progress = new_rec }; + return new_rec; + } + return rec_id; + }, + } } - const rec_id = try self.reserveRecursive(); - try nominal_cycle_breakers.put(nominal_var, rec_id); + // 3. Cache miss: mark in-progress before recursing. + try self.nominal_cache.put(self.allocator, cache_key, .{ .in_progress = TypeId.none }); + // 4. Push nominal arg specializations (args already lowered by buildNominalCacheKey). const named_specializations_top = try self.pushNominalArgSpecializations( types_store, nominal, @@ -981,15 +1059,34 @@ pub const TypeInterner = struct { ); defer scratches.named_specializations.clearFrom(named_specializations_top); + // 5. Lower the backing var. const backing_var = types_store.getNominalBackingVar(nominal); - const backing_idx = try self.fromTypeVar(self.allocator, types_store, backing_var, common_idents, specializations, nominal_cycle_breakers, scratches); - - self.finalizeRecursive(rec_id, backing_idx); - - if (std.debug.runtime_safety) { - std.debug.assert(!backing_idx.isNone()); + const backing_idx = try self.fromTypeVar( + self.allocator, + types_store, + backing_var, + common_idents, + specializations, + nominal_cycle_breakers, + scratches, + ); + std.debug.assert(!backing_idx.isNone()); + + // 6. Finalize: check if recursion occurred. + // Must re-fetch pointer because recursive lowering may have grown the map. + const entry_ptr = self.nominal_cache.getPtr(cache_key).?; + const in_progress_rec = entry_ptr.in_progress; + + if (!in_progress_rec.isNone()) { + // Recursive: finalize the .rec slot, cache the .rec TypeId. + self.finalizeRecursive(in_progress_rec, backing_idx); + entry_ptr.* = .{ .resolved = in_progress_rec }; + return in_progress_rec; + } else { + // Non-recursive: cache the backing type directly. + entry_ptr.* = .{ .resolved = backing_idx }; + return backing_idx; } - return rec_id; } fn lookupNamedSpecialization(scratches: *const Scratches, name: Ident.Idx) ?TypeId { @@ -1161,41 +1258,5 @@ pub const TypeInterner = struct { } }; -fn findEquivalentNominalCycleBreaker( - types_store: *const types.Store, - nominal: types.NominalType, - nominal_cycle_breakers: *const std.AutoHashMap(types.Var, TypeId), -) ?TypeId { - var iter = nominal_cycle_breakers.iterator(); - while (iter.next()) |entry| { - const resolved = types_store.resolveVar(entry.key_ptr.*); - if (resolved.desc.content != .structure) continue; - const flat = resolved.desc.content.structure; - if (flat != .nominal_type) continue; - const other_nominal = flat.nominal_type; - - if (!nominal.origin_module.eql(other_nominal.origin_module)) continue; - if (!nominal.ident.ident_idx.eql(other_nominal.ident.ident_idx)) continue; - - const lhs_args = types_store.sliceNominalArgs(nominal); - const rhs_args = types_store.sliceNominalArgs(other_nominal); - if (lhs_args.len != rhs_args.len) continue; - - var args_match = true; - for (lhs_args, rhs_args) |lhs_arg, rhs_arg| { - const lhs_resolved = types_store.resolveVar(lhs_arg); - const rhs_resolved = types_store.resolveVar(rhs_arg); - if (lhs_resolved.var_ != rhs_resolved.var_) { - args_match = false; - break; - } - } - - if (args_match) return entry.value_ptr.*; - } - - return null; -} - /// Backward-compat alias. pub const Store = TypeInterner; diff --git a/src/mir/test/lower_test.zig b/src/mir/test/lower_test.zig index 8fd4edb028b..b4df782589a 100644 --- a/src/mir/test/lower_test.zig +++ b/src/mir/test/lower_test.zig @@ -2792,3 +2792,38 @@ test "Dec.abs lowers to num_abs with Dec monotype, not unit" { try testing.expectEqual(Monotype.Prim.dec, ret_id.builtinPrim().?); } } + +// --- Nominal instance cache tests --- + +test "nominal cache: non-recursive nominal returns direct type (not .rec)" { + var env = try MirTestEnv.initModule("Test", + \\Color := [Red, Green, Blue] + \\ + \\x : Color + \\x = Color.Red + ); + defer env.deinit(); + const expr = try env.lowerNamedDef("x"); + const raw_type = env.mir_store.typeOf(expr); + // Non-recursive nominal should NOT produce a .rec indirection. + try testing.expect(raw_type.kind == .tag_union); +} + +test "nominal cache: identical nominal instantiations share TypeId" { + var env = try MirTestEnv.initModule("Test", + \\Pair(a) := { fst: a, snd: a } + \\ + \\x : Pair(U64) + \\x = { fst: 1, snd: 2 } + \\ + \\y : Pair(U64) + \\y = { fst: 3, snd: 4 } + ); + defer env.deinit(); + const expr_x = try env.lowerNamedDef("x"); + const expr_y = try env.lowerNamedDef("y"); + const type_x = env.mir_store.typeOf(expr_x); + const type_y = env.mir_store.typeOf(expr_y); + // Same nominal with same args should return the exact same TypeId. + try testing.expect(type_x.eql(type_y)); +} From 547bcfeaf819eaa97bd74bcc3fad14487174d3d2 Mon Sep 17 00:00:00 2001 From: "Luke Boswell (Linux-Desktop)" Date: Thu, 19 Mar 2026 16:47:37 +1100 Subject: [PATCH 4/8] Remove nominal_cycle_breakers now that content-based interning handles nominal dedup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The separate cycle-breaking map for recursive nominal types is no longer needed — the nominal instance cache introduced in the prior commit already prevents infinite recursion by caching based on content identity. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/layout/store_test.zig | 3 --- src/mir/Lower.zig | 49 --------------------------------------- src/mir/Monotype.zig | 42 ++++++++++++--------------------- 3 files changed, 15 insertions(+), 79 deletions(-) diff --git a/src/layout/store_test.zig b/src/layout/store_test.zig index 5508e60b4c9..878d70b8158 100644 --- a/src/layout/store_test.zig +++ b/src/layout/store_test.zig @@ -99,8 +99,6 @@ fn expectTypeAndMonotypeResolversAgree( var specializations = std.AutoHashMap(types.Var, mir.Monotype.Idx).init(allocator); defer specializations.deinit(); - var nominal_cycle_breakers = std.AutoHashMap(types.Var, mir.Monotype.Idx).init(allocator); - defer nominal_cycle_breakers.deinit(); const mono_idx = try mono_store.fromTypeVar( allocator, @@ -108,7 +106,6 @@ fn expectTypeAndMonotypeResolversAgree( type_var, lt.module_env.idents, &specializations, - &nominal_cycle_breakers, &scratches, ); diff --git a/src/mir/Lower.zig b/src/mir/Lower.zig index dfef7d685a5..66ba6038f5e 100644 --- a/src/mir/Lower.zig +++ b/src/mir/Lower.zig @@ -104,11 +104,6 @@ symbol_monotypes: std.AutoHashMap(u64, Monotype.Idx), /// Written by `bindTypeVarMonotypes`, read by `fromTypeVar`. type_var_seen: std.AutoHashMap(types.Var, Monotype.Idx), -/// Cycle breakers for recursive nominal types (e.g. Tree := [Leaf, Node(Tree)]). -/// Used only by `fromNominalType` during monotype construction; separate from -/// specialization bindings so monotype construction never pollutes them. -nominal_cycle_breakers: std.AutoHashMap(types.Var, Monotype.Idx), - /// Cache for already-lowered symbol definitions (avoids re-lowering). /// Key is @bitCast(MIR.Symbol) → u64. lowered_symbols: std.AutoHashMap(u64, MIR.ExprId), @@ -194,7 +189,6 @@ pub fn init( .pattern_symbols = std.AutoHashMap(u128, MIR.Symbol).init(allocator), .symbol_monotypes = std.AutoHashMap(u64, Monotype.Idx).init(allocator), .type_var_seen = std.AutoHashMap(types.Var, Monotype.Idx).init(allocator), - .nominal_cycle_breakers = std.AutoHashMap(types.Var, Monotype.Idx).init(allocator), .lowered_symbols = std.AutoHashMap(u64, MIR.ExprId).init(allocator), .poly_specializations = std.AutoHashMap(u128, MIR.Symbol).init(allocator), .symbol_metadata = std.AutoHashMap(u64, SymbolMetadata).init(allocator), @@ -225,7 +219,6 @@ pub fn deinit(self: *Self) void { self.pattern_symbols.deinit(); self.symbol_monotypes.deinit(); self.type_var_seen.deinit(); - self.nominal_cycle_breakers.deinit(); self.lowered_symbols.deinit(); self.poly_specializations.deinit(); self.symbol_metadata.deinit(); @@ -840,15 +833,11 @@ fn monotypeFromTypeVarInStoreWithBindings( var_: types.Var, bindings: *std.AutoHashMap(types.Var, Monotype.Idx), ) Allocator.Error!Monotype.Idx { - var local_cycles = std.AutoHashMap(types.Var, Monotype.Idx).init(self.allocator); - defer local_cycles.deinit(); - return self.monotypeFromTypeVarWithBindings( module_idx, store_types, var_, bindings, - &local_cycles, ); } @@ -1031,7 +1020,6 @@ fn monotypeFromTypeVarWithBindings( store_types: *const types.Store, var_: types.Var, bindings: *std.AutoHashMap(types.Var, Monotype.Idx), - cycles: *std.AutoHashMap(types.Var, Monotype.Idx), ) Allocator.Error!Monotype.Idx { const saved_ident_store = self.mono_scratches.ident_store; self.mono_scratches.ident_store = self.all_module_envs[module_idx].getIdentStoreConst(); @@ -1046,7 +1034,6 @@ fn monotypeFromTypeVarWithBindings( var_, ModuleEnv.CommonIdents.find(&self.all_module_envs[module_idx].common), bindings, - cycles, &self.mono_scratches, ); } @@ -2327,13 +2314,9 @@ pub fn lowerExpr(self: *Self, expr_idx: CIR.Expr.Idx) Allocator.Error!MIR.ExprId const saved_type_var_seen = self.type_var_seen; self.type_var_seen = std.AutoHashMap(types.Var, Monotype.Idx).init(self.allocator); - const saved_nominal_cycle_breakers = self.nominal_cycle_breakers; - self.nominal_cycle_breakers = std.AutoHashMap(types.Var, Monotype.Idx).init(self.allocator); defer { self.type_var_seen.deinit(); self.type_var_seen = saved_type_var_seen; - self.nominal_cycle_breakers.deinit(); - self.nominal_cycle_breakers = saved_nominal_cycle_breakers; } try self.bindTypeVarMonotypes(ModuleEnv.varFrom(cir_def_expr), monotype); const saved_pattern_scope = self.current_pattern_scope; @@ -2368,7 +2351,6 @@ pub fn lowerExpr(self: *Self, expr_idx: CIR.Expr.Idx) Allocator.Error!MIR.ExprId self.types_store, lookup_var, &self.type_var_seen, - &self.nominal_cycle_breakers, ); if (pattern_monotype == monotype) { return try self.store.addExpr(self.allocator, .{ .lookup = symbol }, pattern_monotype, region); @@ -2859,7 +2841,6 @@ fn resolveMonotype(self: *Self, expr_idx: CIR.Expr.Idx) Allocator.Error!Monotype self.types_store, type_var, &self.type_var_seen, - &self.nominal_cycle_breakers, ); } @@ -2913,7 +2894,6 @@ fn lowerPattern(self: *Self, module_env: *const ModuleEnv, pattern_idx: CIR.Patt self.types_store, type_var, &self.type_var_seen, - &self.nominal_cycle_breakers, ); const lowered = switch (pattern) { @@ -3151,7 +3131,6 @@ fn lowerClosure(self: *Self, module_env: *const ModuleEnv, closure: CIR.Expr.Clo self.types_store, cap_type_var, &self.type_var_seen, - &self.nominal_cycle_breakers, ); try self.mono_scratches.idxs.append(cap_monotype); @@ -3334,13 +3313,9 @@ fn lowerDeferredBlockLambda( // contaminated by earlier entries from this block/module. const saved_type_var_seen = self.type_var_seen; self.type_var_seen = std.AutoHashMap(types.Var, Monotype.Idx).init(self.allocator); - const saved_nominal_cycle_breakers = self.nominal_cycle_breakers; - self.nominal_cycle_breakers = std.AutoHashMap(types.Var, Monotype.Idx).init(self.allocator); defer { self.type_var_seen.deinit(); self.type_var_seen = saved_type_var_seen; - self.nominal_cycle_breakers.deinit(); - self.nominal_cycle_breakers = saved_nominal_cycle_breakers; } if (!caller_monotype.isNone()) { @@ -4512,21 +4487,9 @@ fn lowerDotAccess(self: *Self, module_env: *const ModuleEnv, expr_idx: CIR.Expr. } } self.type_var_seen = call_type_var_seen; - const saved_nominal_cycle_breakers = self.nominal_cycle_breakers; - var call_nominal_cycle_breakers = std.AutoHashMap(types.Var, Monotype.Idx).init(self.allocator); - errdefer call_nominal_cycle_breakers.deinit(); - { - var it = saved_nominal_cycle_breakers.iterator(); - while (it.next()) |entry| { - try call_nominal_cycle_breakers.put(entry.key_ptr.*, entry.value_ptr.*); - } - } - self.nominal_cycle_breakers = call_nominal_cycle_breakers; defer { self.type_var_seen.deinit(); self.type_var_seen = saved_type_var_seen; - self.nominal_cycle_breakers.deinit(); - self.nominal_cycle_breakers = saved_nominal_cycle_breakers; } var method_effectful = false; @@ -4573,7 +4536,6 @@ fn lowerDotAccess(self: *Self, module_env: *const ModuleEnv, expr_idx: CIR.Expr. self.types_store, resolved_target.fn_var, &self.type_var_seen, - &self.nominal_cycle_breakers, ); if (!dispatch_func_monotype.isNone()) { const dispatch_resolved = self.store.monotype_store.resolve(dispatch_func_monotype); @@ -4618,7 +4580,6 @@ fn lowerDotAccess(self: *Self, module_env: *const ModuleEnv, expr_idx: CIR.Expr. self.types_store, resolved_target.fn_var, &self.type_var_seen, - &self.nominal_cycle_breakers, ); if (!refined.isNone()) { if (self.store.monotype_store.resolve(refined).kind != .func) { @@ -5000,7 +4961,6 @@ fn resolveDispatchTargetForDotCall( self.types_store, constraint.fn_var, &self.type_var_seen, - &self.nominal_cycle_breakers, ); if (fn_mono.isNone()) { if (!constraint.resolved_target.isNone()) { @@ -5051,7 +5011,6 @@ fn resolveUnresolvedTypeVarDispatchTarget( self.types_store, constraint.fn_var, &self.type_var_seen, - &self.nominal_cycle_breakers, ); if (desired_func_monotype.isNone()) { if (std.debug.runtime_safety) { @@ -5149,8 +5108,6 @@ fn monotypeFromTypeVarInStore( ) Allocator.Error!Monotype.Idx { var local_seen = std.AutoHashMap(types.Var, Monotype.Idx).init(self.allocator); defer local_seen.deinit(); - var local_cycles = std.AutoHashMap(types.Var, Monotype.Idx).init(self.allocator); - defer local_cycles.deinit(); const saved_ident_store = self.mono_scratches.ident_store; const saved_module_env = self.mono_scratches.module_env; @@ -5166,7 +5123,6 @@ fn monotypeFromTypeVarInStore( store_types, var_, &local_seen, - &local_cycles, ); } @@ -5431,7 +5387,6 @@ fn lowerExternalDefWithType(self: *Self, symbol: MIR.Symbol, cir_expr_idx: CIR.E self.types_store, target_type_var, &self.type_var_seen, - &self.nominal_cycle_breakers, ); if (!derived.isNone()) break :blk derived; @@ -5457,7 +5412,6 @@ fn lowerExternalDefWithType(self: *Self, symbol: MIR.Symbol, cir_expr_idx: CIR.E const saved_pattern_scope = self.current_pattern_scope; const saved_types_store = self.types_store; const saved_type_var_seen = self.type_var_seen; - const saved_nominal_cycle_breakers = self.nominal_cycle_breakers; const saved_ident_store = self.mono_scratches.ident_store; const saved_module_env = self.mono_scratches.module_env; self.current_pattern_scope = symbol_key; @@ -5472,7 +5426,6 @@ fn lowerExternalDefWithType(self: *Self, symbol: MIR.Symbol, cir_expr_idx: CIR.E // Reusing a shared cache across polymorphic specializations can pin flex // and rigid vars to an earlier specialization. self.type_var_seen = std.AutoHashMap(types.Var, Monotype.Idx).init(self.allocator); - self.nominal_cycle_breakers = std.AutoHashMap(types.Var, Monotype.Idx).init(self.allocator); try self.seedTypeScopeBindingsInStore( self.current_module_idx, @@ -5497,8 +5450,6 @@ fn lowerExternalDefWithType(self: *Self, symbol: MIR.Symbol, cir_expr_idx: CIR.E defer { self.type_var_seen.deinit(); self.type_var_seen = saved_type_var_seen; - self.nominal_cycle_breakers.deinit(); - self.nominal_cycle_breakers = saved_nominal_cycle_breakers; self.current_pattern_scope = saved_pattern_scope; if (switching_module) { self.types_store = saved_types_store; diff --git a/src/mir/Monotype.zig b/src/mir/Monotype.zig index 1e2d2c3ffef..323620da24d 100644 --- a/src/mir/Monotype.zig +++ b/src/mir/Monotype.zig @@ -644,14 +644,12 @@ pub const TypeInterner = struct { type_var: types.Var, common_idents: CommonIdents, specializations: *const std.AutoHashMap(types.Var, TypeId), - nominal_cycle_breakers: *std.AutoHashMap(types.Var, TypeId), scratches: *Scratches, ) Allocator.Error!TypeId { _ = allocator; // TypeInterner uses self.allocator const resolved = types_store.resolveVar(type_var); if (specializations.get(resolved.var_)) |cached| return cached; - if (nominal_cycle_breakers.get(resolved.var_)) |cached| return cached; return switch (resolved.desc.content) { .flex => |flex| { @@ -670,10 +668,10 @@ pub const TypeInterner = struct { }, .alias => |alias| { const backing_var = types_store.getAliasBackingVar(alias); - return try self.fromTypeVar(self.allocator, types_store, backing_var, common_idents, specializations, nominal_cycle_breakers, scratches); + return try self.fromTypeVar(self.allocator, types_store, backing_var, common_idents, specializations, scratches); }, .structure => |flat_type| { - return try self.fromFlatType(types_store, flat_type, common_idents, specializations, nominal_cycle_breakers, scratches); + return try self.fromFlatType(types_store, flat_type, common_idents, specializations, scratches); }, .err => return self.unit_idx, }; @@ -685,12 +683,11 @@ pub const TypeInterner = struct { flat_type: types.FlatType, common_idents: CommonIdents, specializations: *const std.AutoHashMap(types.Var, TypeId), - nominal_cycle_breakers: *std.AutoHashMap(types.Var, TypeId), scratches: *Scratches, ) Allocator.Error!TypeId { return switch (flat_type) { .nominal_type => |nominal| { - return try self.fromNominalType(types_store, nominal, common_idents, specializations, nominal_cycle_breakers, scratches); + return try self.fromNominalType(types_store, nominal, common_idents, specializations, scratches); }, .empty_record => self.unit_idx, .empty_tag_union => try self.internTagUnion(&.{}), @@ -716,7 +713,7 @@ pub const TypeInterner = struct { } if (seen_name) continue; - const field_type = try self.fromTypeVar(self.allocator, types_store, field_var, common_idents, specializations, nominal_cycle_breakers, scratches); + const field_type = try self.fromTypeVar(self.allocator, types_store, field_var, common_idents, specializations, scratches); const sname = try self.internNameFromIdent(scratches.ident_store.?, name); try scratches.fields.append(.{ .name = sname, .ty = field_type }); } @@ -750,7 +747,7 @@ pub const TypeInterner = struct { } if (seen_name) continue; - const field_type = try self.fromTypeVar(self.allocator, types_store, field_var, common_idents, specializations, nominal_cycle_breakers, scratches); + const field_type = try self.fromTypeVar(self.allocator, types_store, field_var, common_idents, specializations, scratches); const sname = try self.internNameFromIdent(scratches.ident_store.?, name); try scratches.fields.append(.{ .name = sname, .ty = field_type }); } @@ -802,7 +799,7 @@ pub const TypeInterner = struct { defer scratches.fields.clearFrom(scratch_top); for (names, vars) |name, field_var| { - const field_type = try self.fromTypeVar(self.allocator, types_store, field_var, common_idents, specializations, nominal_cycle_breakers, scratches); + const field_type = try self.fromTypeVar(self.allocator, types_store, field_var, common_idents, specializations, scratches); const sname = try self.internNameFromIdent(scratches.ident_store.?, name); try scratches.fields.append(.{ .name = sname, .ty = field_type }); } @@ -817,7 +814,7 @@ pub const TypeInterner = struct { defer scratches.idxs.clearFrom(scratch_top); for (elem_vars) |elem_var| { - const elem_type = try self.fromTypeVar(self.allocator, types_store, elem_var, common_idents, specializations, nominal_cycle_breakers, scratches); + const elem_type = try self.fromTypeVar(self.allocator, types_store, elem_var, common_idents, specializations, scratches); try scratches.idxs.append(elem_type); } @@ -839,7 +836,7 @@ pub const TypeInterner = struct { defer scratches.idxs.clearFrom(idxs_top); for (arg_vars) |arg_var| { - const payload_type = try self.fromTypeVar(self.allocator, types_store, arg_var, common_idents, specializations, nominal_cycle_breakers, scratches); + const payload_type = try self.fromTypeVar(self.allocator, types_store, arg_var, common_idents, specializations, scratches); try scratches.idxs.append(payload_type); } @@ -898,9 +895,9 @@ pub const TypeInterner = struct { std.mem.sort(TagKey, collected_tags, &self.name_pool, TagKey.sortByNameAsc); return try self.internTagUnion(collected_tags); }, - .fn_pure => |func| try self.fromFuncType(types_store, func, false, common_idents, specializations, nominal_cycle_breakers, scratches), - .fn_effectful => |func| try self.fromFuncType(types_store, func, true, common_idents, specializations, nominal_cycle_breakers, scratches), - .fn_unbound => |func| try self.fromFuncType(types_store, func, false, common_idents, specializations, nominal_cycle_breakers, scratches), + .fn_pure => |func| try self.fromFuncType(types_store, func, false, common_idents, specializations, scratches), + .fn_effectful => |func| try self.fromFuncType(types_store, func, true, common_idents, specializations, scratches), + .fn_unbound => |func| try self.fromFuncType(types_store, func, false, common_idents, specializations, scratches), }; } @@ -911,7 +908,6 @@ pub const TypeInterner = struct { effectful: bool, common_idents: CommonIdents, specializations: *const std.AutoHashMap(types.Var, TypeId), - nominal_cycle_breakers: *std.AutoHashMap(types.Var, TypeId), scratches: *Scratches, ) Allocator.Error!TypeId { const arg_vars = types_store.sliceVars(func.args); @@ -919,11 +915,11 @@ pub const TypeInterner = struct { defer scratches.idxs.clearFrom(scratch_top); for (arg_vars) |arg_var| { - const arg_type = try self.fromTypeVar(self.allocator, types_store, arg_var, common_idents, specializations, nominal_cycle_breakers, scratches); + const arg_type = try self.fromTypeVar(self.allocator, types_store, arg_var, common_idents, specializations, scratches); try scratches.idxs.append(arg_type); } - const ret = try self.fromTypeVar(self.allocator, types_store, func.ret, common_idents, specializations, nominal_cycle_breakers, scratches); + const ret = try self.fromTypeVar(self.allocator, types_store, func.ret, common_idents, specializations, scratches); return try self.internFunc(scratches.idxs.sliceFromStart(scratch_top), ret, effectful); } @@ -933,7 +929,6 @@ pub const TypeInterner = struct { nominal: types.NominalType, common_idents: CommonIdents, specializations: *const std.AutoHashMap(types.Var, TypeId), - nominal_cycle_breakers: *std.AutoHashMap(types.Var, TypeId), scratches: *Scratches, ) Allocator.Error!NominalInstanceKey { const ident_store = if (scratches.module_env) |env| env.getIdentStoreConst() else scratches.ident_store.?; @@ -952,7 +947,6 @@ pub const TypeInterner = struct { arg_var, common_idents, specializations, - nominal_cycle_breakers, scratches, ); try scratches.idxs.append(arg_mono); @@ -972,7 +966,6 @@ pub const TypeInterner = struct { nominal: types.NominalType, common_idents: CommonIdents, specializations: *const std.AutoHashMap(types.Var, TypeId), - nominal_cycle_breakers: *std.AutoHashMap(types.Var, TypeId), scratches: *Scratches, ) Allocator.Error!TypeId { const ident = nominal.ident.ident_idx; @@ -987,7 +980,7 @@ pub const TypeInterner = struct { if (ident.eql(common_idents.list)) { const type_args = types_store.sliceNominalArgs(nominal); if (type_args.len > 0) { - const elem_type = try self.fromTypeVar(self.allocator, types_store, type_args[0], common_idents, specializations, nominal_cycle_breakers, scratches); + const elem_type = try self.fromTypeVar(self.allocator, types_store, type_args[0], common_idents, specializations, scratches); return try self.internList(elem_type); } return self.unit_idx; @@ -996,7 +989,7 @@ pub const TypeInterner = struct { if (ident.eql(common_idents.box)) { const type_args = types_store.sliceNominalArgs(nominal); if (type_args.len > 0) { - const inner_type = try self.fromTypeVar(self.allocator, types_store, type_args[0], common_idents, specializations, nominal_cycle_breakers, scratches); + const inner_type = try self.fromTypeVar(self.allocator, types_store, type_args[0], common_idents, specializations, scratches); return try self.internBox(inner_type); } return self.unit_idx; @@ -1024,7 +1017,6 @@ pub const TypeInterner = struct { nominal, common_idents, specializations, - nominal_cycle_breakers, scratches, ); @@ -1054,7 +1046,6 @@ pub const TypeInterner = struct { nominal, common_idents, specializations, - nominal_cycle_breakers, scratches, ); defer scratches.named_specializations.clearFrom(named_specializations_top); @@ -1067,7 +1058,6 @@ pub const TypeInterner = struct { backing_var, common_idents, specializations, - nominal_cycle_breakers, scratches, ); std.debug.assert(!backing_idx.isNone()); @@ -1169,7 +1159,6 @@ pub const TypeInterner = struct { nominal: types.NominalType, common_idents: CommonIdents, specializations: *const std.AutoHashMap(types.Var, TypeId), - nominal_cycle_breakers: *std.AutoHashMap(types.Var, TypeId), scratches: *Scratches, ) Allocator.Error!u32 { const top = scratches.named_specializations.top(); @@ -1200,7 +1189,6 @@ pub const TypeInterner = struct { actual_arg, common_idents, specializations, - nominal_cycle_breakers, scratches, ); try scratches.named_specializations.append(.{ From fa349cc2efd0dd2cbcee8a21818bed202655e173 Mon Sep 17 00:00:00 2001 From: "Luke Boswell (Linux-Desktop)" Date: Thu, 19 Mar 2026 17:29:01 +1100 Subject: [PATCH 5/8] Document cross-module recursive TypeId canonicalization and add tests The `remapMonotypeBetweenModules` function was investigated for removal but proven necessary: it deeply resolves `.rec` placeholder indirections in TypeId children that arise from recursive nominal types. Without this, cross-module TypeId comparisons fail when one module holds `List(rec(0))` while another holds the equivalent `List(tag_union(1))`. Added doc comments explaining the actual purpose (`.rec` resolution, not name/index remapping), regression test comments, and two new cross-module mono tests covering recursive nominal types through import chains. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/check/test/cross_module_mono_test.zig | 76 ++++++++++++++++++++++ src/mir/Lower.zig | 21 ++++++ test/fx/cross_module_recursive_nominal.roc | 10 +++ 3 files changed, 107 insertions(+) diff --git a/src/check/test/cross_module_mono_test.zig b/src/check/test/cross_module_mono_test.zig index c4ee0255c01..b3800aeccf3 100644 --- a/src/check/test/cross_module_mono_test.zig +++ b/src/check/test/cross_module_mono_test.zig @@ -562,6 +562,82 @@ test "cross-module mono: static dispatch with chained method calls" { try testing.expect(main_ident != null); } +test "cross-module mono: recursive nominal type with self-referencing children" { + // Module A defines a recursive nominal type where children reference + // the type itself (Elem contains List(Elem)). This pattern is the key + // scenario where `remapMonotypeBetweenModules` in MIR Lower.zig does + // meaningful work: the TypeId for List(Elem) may contain a `.rec` + // placeholder indirection internally, which must be resolved when used + // from a different module for TypeId comparisons to succeed. + const source_a = + \\Elem := [Div(List(Elem)), Text(Str)].{ + \\ div : List(Elem) -> Elem + \\ div = |children| Div(children) + \\ + \\ text : Str -> Elem + \\ text = |content| Text(content) + \\} + ; + var env_a = try MonoTestEnv.init("Elem", source_a); + defer env_a.deinit(); + + // Module B imports Elem and uses both constructors + const source_b = + \\import Elem + \\ + \\main : Elem + \\main = Elem.div([Elem.text("hello")]) + ; + var env_b = try MonoTestEnv.initWithImport("B", source_b, "Elem", &env_a); + defer env_b.deinit(); + + // Type-check should succeed — the recursive nominal type is usable cross-module + const main_ident = env_b.module_env.common.findIdent("main"); + try testing.expect(main_ident != null); +} + +test "cross-module mono: recursive nominal through 3-module chain" { + // Recursive nominal type used transitively: A defines the type, + // B wraps it, C uses B's wrapper. This exercises cross-module + // TypeId canonicalization across multiple module boundaries. + const source_a = + \\Tree := [Leaf(U64), Branch(List(Tree))].{ + \\ leaf : U64 -> Tree + \\ leaf = |n| Leaf(n) + \\ + \\ branch : List(Tree) -> Tree + \\ branch = |children| Branch(children) + \\} + ; + var env_a = try MonoTestEnv.init("Tree", source_a); + defer env_a.deinit(); + + const source_b = + \\import Tree + \\ + \\make_pair : U64, U64 -> Tree + \\make_pair = |a, b| Tree.branch([Tree.leaf(a), Tree.leaf(b)]) + ; + var env_b = try MonoTestEnv.initWithImport("B", source_b, "Tree", &env_a); + defer env_b.deinit(); + + const source_c = + \\import B + \\import Tree + \\ + \\main : Tree + \\main = B.make_pair(1, 2) + ; + var env_c = try MonoTestEnv.initWithImports("C", source_c, &.{ + .{ .name = "B", .env = &env_b }, + .{ .name = "Tree", .env = &env_a }, + }); + defer env_c.deinit(); + + const main_ident = env_c.module_env.common.findIdent("main"); + try testing.expect(main_ident != null); +} + test "type checker catches polymorphic recursion (infinite type)" { // This test verifies that polymorphic recursion (f = |x| f([x])) is caught // during type checking as a circular/infinite type. diff --git a/src/mir/Lower.zig b/src/mir/Lower.zig index 66ba6038f5e..4d1a8b5c00d 100644 --- a/src/mir/Lower.zig +++ b/src/mir/Lower.zig @@ -323,6 +323,21 @@ fn identFromStructuralName(self: *const Self, name_id: Monotype.StructuralNameId return Ident.Idx.NONE; } +/// Canonicalize a cross-module TypeId by deeply resolving `.rec` indirections. +/// +/// With content-based interning and a shared `TypeInterner`/`StructuralNamePool`, +/// names and indices are already canonical across modules — no actual remapping of +/// those occurs. However, recursive types created via `reserveRecursive` + +/// `finalizeRecursive` may leave `.rec` placeholder TypeIds in their children +/// (e.g. `List(rec(0))` instead of `List(tag_union(1))`). When a different module +/// constructs the same type freshly via `intern*`, it produces the resolved form +/// (e.g. `List(tag_union(1))`), causing TypeId mismatches on direct comparison. +/// +/// This function walks the type tree, resolves all `.rec` children via the +/// monotype store's `resolve()`, and re-interns to produce a canonical TypeId +/// where all children are direct (non-`.rec`) references. This is only needed +/// cross-module — within a single module, `.rec` placeholders are used +/// consistently, so TypeId comparisons already work. fn remapMonotypeBetweenModules( self: *Self, monotype: Monotype.Idx, @@ -342,6 +357,8 @@ fn remapMonotypeBetweenModules( ); } +/// Recursive worker for `remapMonotypeBetweenModules`. The `remapped` map +/// tracks already-processed TypeIds to handle cycles in recursive types. fn remapMonotypeBetweenModulesRec( self: *Self, monotype: Monotype.Idx, @@ -427,6 +444,10 @@ fn remapMonotypeBetweenModulesRec( return canonical; } +/// Convenience wrapper: canonicalize a caller's monotype when it will be +/// compared against or used as a key in the context of a symbol from a +/// different module. Same-module calls skip canonicalization since `.rec` +/// placeholders are already consistent within a module. fn normalizeCallerMonotypeForSymbolModule( self: *Self, symbol_module_idx: u32, diff --git a/test/fx/cross_module_recursive_nominal.roc b/test/fx/cross_module_recursive_nominal.roc index 27c87b4dba8..0b1f43919ce 100644 --- a/test/fx/cross_module_recursive_nominal.roc +++ b/test/fx/cross_module_recursive_nominal.roc @@ -1,3 +1,13 @@ +## Regression test for cross-module recursive nominal type resolution. +## +## The Elem type is a recursive nominal defined in another module: +## Elem := [Div(List(Elem)), Text(Str)] +## +## Cross-module, the TypeId for Elem's children may contain `.rec` +## placeholder indirections (e.g. List(rec(0))) that differ from the +## canonical form (List(tag_union(N))) used by the importing module. +## The MIR lowerer's `remapMonotypeBetweenModules` resolves these so +## that polymorphic specialization and type comparisons work correctly. app [main!] { pf: platform "./platform/main.roc", elem: "./elem_pkg/main.roc", From f7e4801e3249405d0b15b599bfc037a7e9fa8cac Mon Sep 17 00:00:00 2001 From: "Luke Boswell (Linux-Desktop)" Date: Thu, 19 Mar 2026 17:50:41 +1100 Subject: [PATCH 6/8] Clean up intern-monotype branch: fix double-resolves, tag comparison bug, dead param - Cache resolve() results in locals where called twice on the same index (MirToLir.zig: 3 sites, Lower.zig: 1 site) - Replace fragile identText() + null-guard + manual eql pattern with identMatchesText() in runtimeTagLayoutFromExpr, eliminating a latent null path that would silently skip all tags - Remove unused allocator parameter from TypeInterner.deinit() - Expand Store alias doc comment Co-Authored-By: Claude Opus 4.6 (1M context) --- src/layout/store_test.zig | 2 +- src/lir/MirToLir.zig | 27 +++++++++++---------------- src/mir/Lower.zig | 5 +++-- src/mir/MIR.zig | 2 +- src/mir/Monotype.zig | 6 +++--- src/mir/test/lower_test.zig | 18 +++++++++--------- 6 files changed, 28 insertions(+), 32 deletions(-) diff --git a/src/layout/store_test.zig b/src/layout/store_test.zig index 878d70b8158..1094640ea01 100644 --- a/src/layout/store_test.zig +++ b/src/layout/store_test.zig @@ -91,7 +91,7 @@ fn expectTypeAndMonotypeResolversAgree( const type_layout_idx = try type_layout_resolver.resolve(0, type_var, <.type_scope, null); var mono_store = try mir.Monotype.Store.init(allocator); - defer mono_store.deinit(allocator); + defer mono_store.deinit(); var scratches = try mir.Monotype.Store.Scratches.init(allocator); defer scratches.deinit(); diff --git a/src/lir/MirToLir.zig b/src/lir/MirToLir.zig index d3270a080bb..02e2058c8af 100644 --- a/src/lir/MirToLir.zig +++ b/src/lir/MirToLir.zig @@ -627,10 +627,10 @@ fn runtimeTagLayoutFromExpr( const save_idxs = self.scratch_layout_idxs.items.len; defer self.scratch_layout_idxs.shrinkRetainingCapacity(save_idxs); - const tag_name_text = self.identText(tag_data.name); var found_active = false; for (tags) |tag| { - if (tag_name_text != null and std.mem.eql(u8, self.mir_store.monotype_store.getNameText(tag.name), tag_name_text.?)) { + const mono_tag_text = self.mir_store.monotype_store.getNameText(tag.name); + if (self.identMatchesText(tag_data.name, mono_tag_text)) { found_active = true; if (mir_args.len == 0) { try self.scratch_layout_idxs.append(self.allocator, zst_idx); @@ -1726,14 +1726,6 @@ fn identTextIfOwnedBy(env: anytype, ident: Ident.Idx) ?[]const u8 { } /// Resolve an Ident.Idx to its text string, searching all module envs. -fn identText(self: *const Self, ident: Ident.Idx) ?[]const u8 { - if (identTextIfOwnedBy(self.layout_store.currentEnv(), ident)) |text| return text; - for (self.layout_store.moduleEnvs()) |env| { - if (identTextIfOwnedBy(env, ident)) |text| return text; - } - return null; -} - fn identMatchesText(self: *const Self, ident: Ident.Idx, expected: []const u8) bool { if (identTextIfOwnedBy(self.layout_store.currentEnv(), ident)) |text| { if (std.mem.eql(u8, text, expected)) return true; @@ -2558,11 +2550,12 @@ fn lowerLookup(self: *Self, sym: Symbol, mono_idx: Monotype.Idx, _: MIR.ExprId, } } + const resolved = self.mir_store.monotype_store.resolve(mono_idx); const layout_idx = blk: { if (self.symbol_layouts.get(sym.raw())) |binding_layout| { break :blk try self.runtimeLayoutForBindingSymbol(sym, mono_idx, binding_layout); } - if (self.mir_store.monotype_store.resolve(mono_idx).kind == .func) { + if (resolved.kind == .func) { if (self.lambda_set_store.getSymbolLambdaSet(sym)) |ls_idx| { break :blk try self.closureValueLayoutFromLambdaSet(ls_idx); } @@ -2570,7 +2563,7 @@ fn lowerLookup(self: *Self, sym: Symbol, mono_idx: Monotype.Idx, _: MIR.ExprId, if (self.mir_store.getSymbolDef(sym)) |mir_def_id| { break :blk try self.runtimeValueLayoutFromMirExpr(mir_def_id); } - if (self.mir_store.monotype_store.resolve(mono_idx).kind == .func) { + if (resolved.kind == .func) { break :blk try self.layoutFromMonotype(mono_idx); } break :blk try self.layoutFromMonotype(mono_idx); @@ -4460,9 +4453,10 @@ fn registerBindingPatternSymbols( .struct_destructure => |sd| { const mir_patterns = self.mir_store.getPatternSpan(sd.fields); if (mir_patterns.len == 0) return; - switch (self.mir_store.monotype_store.resolve(mono_idx).kind) { + const resolved = self.mir_store.monotype_store.resolve(mono_idx); + switch (resolved.kind) { .record => { - const all_fields = self.mir_store.monotype_store.recordFields(self.mir_store.monotype_store.resolve(mono_idx)); + const all_fields = self.mir_store.monotype_store.recordFields(resolved); const record_layout_val = self.layout_store.getLayout(runtime_layout); if (builtin.mode == .Debug and all_fields.len != 0 and record_layout_val.tag != .struct_) { std.debug.panic( @@ -4644,9 +4638,10 @@ fn lowerPatternInternal( break :blk self.lowerWildcardBindingPattern(struct_layout, ownership_mode, region); } - switch (self.mir_store.monotype_store.resolve(mono_idx).kind) { + const resolved = self.mir_store.monotype_store.resolve(mono_idx); + switch (resolved.kind) { .record => { - const all_fields = self.mir_store.monotype_store.recordFields(self.mir_store.monotype_store.resolve(mono_idx)); + const all_fields = self.mir_store.monotype_store.recordFields(resolved); if (all_fields.len == 0) { break :blk self.lowerWildcardBindingPattern(struct_layout, ownership_mode, region); diff --git a/src/mir/Lower.zig b/src/mir/Lower.zig index 4d1a8b5c00d..ed2289d5490 100644 --- a/src/mir/Lower.zig +++ b/src/mir/Lower.zig @@ -4603,10 +4603,11 @@ fn lowerDotAccess(self: *Self, module_env: *const ModuleEnv, expr_idx: CIR.Expr. &self.type_var_seen, ); if (!refined.isNone()) { - if (self.store.monotype_store.resolve(refined).kind != .func) { + const resolved_refined = self.store.monotype_store.resolve(refined); + if (resolved_refined.kind != .func) { typeBindingInvariant( "lowerDotAccess: refined dispatch fn_var monotype is not function (method='{s}', kind={d})", - .{ module_env.getIdent(da.field_name), @intFromEnum(self.store.monotype_store.resolve(refined).kind) }, + .{ module_env.getIdent(da.field_name), @intFromEnum(resolved_refined.kind) }, ); } break :blk refined; diff --git a/src/mir/MIR.zig b/src/mir/MIR.zig index 4e2b984447c..40ee85e8b87 100644 --- a/src/mir/MIR.zig +++ b/src/mir/MIR.zig @@ -604,7 +604,7 @@ pub const Store = struct { self.borrow_bindings.deinit(allocator); self.captures.deinit(allocator); self.capture_bindings.deinit(allocator); - self.monotype_store.deinit(allocator); + self.monotype_store.deinit(); self.closure_members.deinit(allocator); self.expr_closure_members.deinit(allocator); self.fn_closure_members.deinit(allocator); diff --git a/src/mir/Monotype.zig b/src/mir/Monotype.zig index 323620da24d..4a5b116edcd 100644 --- a/src/mir/Monotype.zig +++ b/src/mir/Monotype.zig @@ -386,8 +386,7 @@ pub const TypeInterner = struct { return self; } - pub fn deinit(self: *TypeInterner, allocator: Allocator) void { - _ = allocator; // TypeInterner uses self.allocator + pub fn deinit(self: *TypeInterner) void { self.list_payloads.deinit(self.allocator); self.box_payloads.deinit(self.allocator); self.tuple_payloads.deinit(self.allocator); @@ -1246,5 +1245,6 @@ pub const TypeInterner = struct { } }; -/// Backward-compat alias. +/// Backward-compat alias used in 17+ call sites (MIR.zig, Lower.zig, MirToLir.zig, etc.). +/// May be removed in a future rename pass. pub const Store = TypeInterner; diff --git a/src/mir/test/lower_test.zig b/src/mir/test/lower_test.zig index b4df782589a..c84ef79137e 100644 --- a/src/mir/test/lower_test.zig +++ b/src/mir/test/lower_test.zig @@ -218,7 +218,7 @@ test "MIR Store: multiple expressions round trip" { test "Monotype Store: primitive types" { var store = try Monotype.Store.init(test_allocator); - defer store.deinit(test_allocator); + defer store.deinit(); try testing.expectEqual(Monotype.Prim.str, store.primIdx(.str).builtinPrim().?); try testing.expectEqual(Monotype.Prim.i64, store.primIdx(.i64).builtinPrim().?); @@ -226,14 +226,14 @@ test "Monotype Store: primitive types" { test "Monotype Store: unit type" { var store = try Monotype.Store.init(test_allocator); - defer store.deinit(test_allocator); + defer store.deinit(); try testing.expect(store.unit_idx.isUnit()); } test "Monotype Store: list type" { var store = try Monotype.Store.init(test_allocator); - defer store.deinit(test_allocator); + defer store.deinit(); const elem = store.primIdx(.str); const list = try store.internList(elem); @@ -244,7 +244,7 @@ test "Monotype Store: list type" { test "Monotype Store: func type" { var store = try Monotype.Store.init(test_allocator); - defer store.deinit(test_allocator); + defer store.deinit(); const arg1 = store.primIdx(.i64); const arg2 = store.primIdx(.str); @@ -259,7 +259,7 @@ test "Monotype Store: func type" { test "Monotype Store: record type" { var store = try Monotype.Store.init(test_allocator); - defer store.deinit(test_allocator); + defer store.deinit(); const field1_type = store.primIdx(.i64); const field2_type = store.primIdx(.str); @@ -276,7 +276,7 @@ test "Monotype Store: record type" { test "Monotype Store: tag union type" { var store = try Monotype.Store.init(test_allocator); - defer store.deinit(test_allocator); + defer store.deinit(); const payload_type = store.primIdx(.str); const tag_name = try store.name_pool.intern("MyTag"); @@ -290,7 +290,7 @@ test "Monotype Store: tag union type" { test "Monotype Store: box type" { var store = try Monotype.Store.init(test_allocator); - defer store.deinit(test_allocator); + defer store.deinit(); const inner = store.primIdx(.i64); const boxed = try store.internBox(inner); @@ -301,7 +301,7 @@ test "Monotype Store: box type" { test "Monotype Store: tuple type" { var store = try Monotype.Store.init(test_allocator); - defer store.deinit(test_allocator); + defer store.deinit(); const elem1 = store.primIdx(.i64); const elem2 = store.primIdx(.str); @@ -313,7 +313,7 @@ test "Monotype Store: tuple type" { test "Monotype Store: all primitive types" { var store = try Monotype.Store.init(test_allocator); - defer store.deinit(test_allocator); + defer store.deinit(); const prims = [_]Monotype.Prim{ .str, From 6bbba25334707035541f86150ca98290e5a55a94 Mon Sep 17 00:00:00 2001 From: "Luke Boswell (Linux-Desktop)" Date: Thu, 19 Mar 2026 18:01:01 +1100 Subject: [PATCH 7/8] Replace linear scans with fingerprint-keyed hash maps in list interning internIdxList, internFieldList, and internTagList were doing O(n) linear scans over all meta entries. Replace with AutoHashMapUnmanaged(u64, ListId) keyed by fingerprint, with overflow chaining via a `next` field on the meta structs to handle fingerprint collisions. This brings list interning to O(1) amortized, matching the existing per-kind type intern maps. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/mir/Monotype.zig | 104 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 83 insertions(+), 21 deletions(-) diff --git a/src/mir/Monotype.zig b/src/mir/Monotype.zig index 4a5b116edcd..fa9dbd0ec29 100644 --- a/src/mir/Monotype.zig +++ b/src/mir/Monotype.zig @@ -297,6 +297,11 @@ pub const TypeInterner = struct { union_map: std.AutoHashMapUnmanaged(TagListId, TypeId), func_map: std.AutoHashMapUnmanaged(u96, TypeId), // packed func key + // Fingerprint → chain-head maps for list interning + idx_list_fp_map: std.AutoHashMapUnmanaged(u64, IdxListId), + field_list_fp_map: std.AutoHashMapUnmanaged(u64, FieldListId), + tag_list_fp_map: std.AutoHashMapUnmanaged(u64, TagListId), + // Structural name pool name_pool: StructuralNamePool, @@ -312,9 +317,9 @@ pub const TypeInterner = struct { // --- Internal list metadata --- - const IdxListMeta = struct { start: u32, len: u32, fingerprint: u64 }; - const FieldListMeta = struct { start: u32, len: u32, fingerprint: u64 }; - const TagListMeta = struct { start: u32, len: u32, fingerprint: u64 }; + const IdxListMeta = struct { start: u32, len: u32, fingerprint: u64, next: IdxListId }; + const FieldListMeta = struct { start: u32, len: u32, fingerprint: u64, next: FieldListId }; + const TagListMeta = struct { start: u32, len: u32, fingerprint: u64, next: TagListId }; // --- Scratches (reusable temporary buffers) --- @@ -370,6 +375,9 @@ pub const TypeInterner = struct { .record_map = .{}, .union_map = .{}, .func_map = .{}, + .idx_list_fp_map = .{}, + .field_list_fp_map = .{}, + .tag_list_fp_map = .{}, .name_pool = StructuralNamePool.init(allocator), .nominal_cache = .{}, .unit_idx = undefined, @@ -406,6 +414,9 @@ pub const TypeInterner = struct { self.record_map.deinit(self.allocator); self.union_map.deinit(self.allocator); self.func_map.deinit(self.allocator); + self.idx_list_fp_map.deinit(self.allocator); + self.field_list_fp_map.deinit(self.allocator); + self.tag_list_fp_map.deinit(self.allocator); self.name_pool.deinit(); self.nominal_cache.deinit(self.allocator); } @@ -502,43 +513,94 @@ pub const TypeInterner = struct { pub fn internIdxList(self: *TypeInterner, items: []const TypeId) !IdxListId { if (items.len == 0) return .empty; const fp = hashTypeIds(items); - for (self.idx_list_metas.items, 0..) |meta, i| { - if (meta.fingerprint != fp or meta.len != items.len) continue; - if (eqlTypeIds(self.idx_list_items.items[meta.start..][0..meta.len], items)) - return @enumFromInt(@as(u32, @intCast(i)) + 1); + + const gop = try self.idx_list_fp_map.getOrPut(self.allocator, fp); + + if (gop.found_existing) { + var cur = gop.value_ptr.*; + while (cur != .empty) { + const meta = self.idx_list_metas.items[@intFromEnum(cur) - 1]; + if (meta.len == items.len and + eqlTypeIds(self.idx_list_items.items[meta.start..][0..meta.len], items)) + return cur; + cur = meta.next; + } } + + const old_head: IdxListId = if (gop.found_existing) gop.value_ptr.* else .empty; const start: u32 = @intCast(self.idx_list_items.items.len); try self.idx_list_items.appendSlice(self.allocator, items); - try self.idx_list_metas.append(self.allocator, .{ .start = start, .len = @intCast(items.len), .fingerprint = fp }); - return @enumFromInt(@as(u32, @intCast(self.idx_list_metas.items.len))); + try self.idx_list_metas.append(self.allocator, .{ + .start = start, + .len = @intCast(items.len), + .fingerprint = fp, + .next = old_head, + }); + const new_id: IdxListId = @enumFromInt(@as(u32, @intCast(self.idx_list_metas.items.len))); + gop.value_ptr.* = new_id; + return new_id; } pub fn internFieldList(self: *TypeInterner, items: []const FieldKey) !FieldListId { if (items.len == 0) return .empty; const fp = hashFieldKeys(items); - for (self.field_list_metas.items, 0..) |meta, i| { - if (meta.fingerprint != fp or meta.len != items.len) continue; - if (eqlFieldKeys(self.field_list_items.items[meta.start..][0..meta.len], items)) - return @enumFromInt(@as(u32, @intCast(i)) + 1); + + const gop = try self.field_list_fp_map.getOrPut(self.allocator, fp); + + if (gop.found_existing) { + var cur = gop.value_ptr.*; + while (cur != .empty) { + const meta = self.field_list_metas.items[@intFromEnum(cur) - 1]; + if (meta.len == items.len and + eqlFieldKeys(self.field_list_items.items[meta.start..][0..meta.len], items)) + return cur; + cur = meta.next; + } } + + const old_head: FieldListId = if (gop.found_existing) gop.value_ptr.* else .empty; const start: u32 = @intCast(self.field_list_items.items.len); try self.field_list_items.appendSlice(self.allocator, items); - try self.field_list_metas.append(self.allocator, .{ .start = start, .len = @intCast(items.len), .fingerprint = fp }); - return @enumFromInt(@as(u32, @intCast(self.field_list_metas.items.len))); + try self.field_list_metas.append(self.allocator, .{ + .start = start, + .len = @intCast(items.len), + .fingerprint = fp, + .next = old_head, + }); + const new_id: FieldListId = @enumFromInt(@as(u32, @intCast(self.field_list_metas.items.len))); + gop.value_ptr.* = new_id; + return new_id; } pub fn internTagList(self: *TypeInterner, items: []const TagKey) !TagListId { if (items.len == 0) return .empty; const fp = hashTagKeys(items); - for (self.tag_list_metas.items, 0..) |meta, i| { - if (meta.fingerprint != fp or meta.len != items.len) continue; - if (eqlTagKeys(self.tag_list_items.items[meta.start..][0..meta.len], items)) - return @enumFromInt(@as(u32, @intCast(i)) + 1); + + const gop = try self.tag_list_fp_map.getOrPut(self.allocator, fp); + + if (gop.found_existing) { + var cur = gop.value_ptr.*; + while (cur != .empty) { + const meta = self.tag_list_metas.items[@intFromEnum(cur) - 1]; + if (meta.len == items.len and + eqlTagKeys(self.tag_list_items.items[meta.start..][0..meta.len], items)) + return cur; + cur = meta.next; + } } + + const old_head: TagListId = if (gop.found_existing) gop.value_ptr.* else .empty; const start: u32 = @intCast(self.tag_list_items.items.len); try self.tag_list_items.appendSlice(self.allocator, items); - try self.tag_list_metas.append(self.allocator, .{ .start = start, .len = @intCast(items.len), .fingerprint = fp }); - return @enumFromInt(@as(u32, @intCast(self.tag_list_metas.items.len))); + try self.tag_list_metas.append(self.allocator, .{ + .start = start, + .len = @intCast(items.len), + .fingerprint = fp, + .next = old_head, + }); + const new_id: TagListId = @enumFromInt(@as(u32, @intCast(self.tag_list_metas.items.len))); + gop.value_ptr.* = new_id; + return new_id; } pub fn getIdxListItems(self: *const TypeInterner, id: IdxListId) []const TypeId { From e925a7b46433f08f85950dcbbe1e9b024e69d131 Mon Sep 17 00:00:00 2001 From: "Luke Boswell (Linux-Desktop)" Date: Thu, 19 Mar 2026 18:16:06 +1100 Subject: [PATCH 8/8] Fix identFromStructuralName to search all module envs for cross-module tags The function only searched the current module's ident store, so cross-module tag names would silently fall through to Ident.Idx.NONE. Now searches all module envs, matching what identMatchesText in MirToLir already does. Also deduplicate the redundant findByString calls into a shared findIdentInEnv helper. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/mir/Lower.zig | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/mir/Lower.zig b/src/mir/Lower.zig index ed2289d5490..f59dff6095c 100644 --- a/src/mir/Lower.zig +++ b/src/mir/Lower.zig @@ -313,16 +313,23 @@ fn identLastSegment(text: []const u8) []const u8 { /// that require Ident.Idx names from monotype tag/field names. fn identFromStructuralName(self: *const Self, name_id: Monotype.StructuralNameId) Ident.Idx { const text = self.store.monotype_store.getNameText(name_id); - const module_env = self.all_module_envs[self.current_module_idx]; - if (module_env.common.findIdent(text)) |ident| return ident; - const ident_store = module_env.getIdentStoreConst(); - if (ident_store.findByString(text)) |ident| return ident; - if (ident_store.lookup(Ident.for_text(text))) |ident| return ident; - // Fallback: return a synthetic ident index; the text comparison path - // in the backend will handle it. + // Search current module first (most common case). + const current_env = self.all_module_envs[self.current_module_idx]; + if (findIdentInEnv(current_env, text)) |ident| return ident; + // Fall back to all other modules for cross-module tags. + for (self.all_module_envs, 0..) |env, i| { + if (i == self.current_module_idx) continue; + if (findIdentInEnv(env, text)) |ident| return ident; + } return Ident.Idx.NONE; } +fn findIdentInEnv(env: *const ModuleEnv, text: []const u8) ?Ident.Idx { + if (env.common.findIdent(text)) |ident| return ident; + if (env.getIdentStoreConst().lookup(Ident.for_text(text))) |ident| return ident; + return null; +} + /// Canonicalize a cross-module TypeId by deeply resolving `.rec` indirections. /// /// With content-based interning and a shared `TypeInterner`/`StructuralNamePool`,