const std = @import("std"); const Allocator = std.mem.Allocator; const ast = @import("../ast.zig"); // ── TypeId ────────────────────────────────────────────────────────────── // Opaque handle into the TypeTable. First 16 slots are reserved for builtins. pub const TypeId = enum(u32) { // Builtin slots 0–17. /// Resolution failed (e.g. an unannotated param whose type was never /// inferred from context). A dedicated sentinel — never a legitimate /// result — so downstream `== .void`/`== .s64` checks can't silently /// swallow it. Must never reach codegen; sizeOf/toLLVMType panic on it. /// /// Deliberately slot 0: a zero-initialised or forgotten `TypeId` (the most /// common accidental value) thus reads as `.unresolved` and trips the /// tripwire, rather than silently masquerading as `.void`. unresolved = 0, bool = 1, s8 = 2, s16 = 3, s32 = 4, s64 = 5, u8 = 6, u16 = 7, u32 = 8, u64 = 9, f32 = 10, f64 = 11, string = 12, // [:0]u8 any = 13, noreturn = 14, isize = 15, usize = 16, void = 17, _, // user-defined types start at 18 pub const first_user: u32 = 18; pub fn index(self: TypeId) u32 { return @intFromEnum(self); } pub fn fromIndex(i: u32) TypeId { return @enumFromInt(i); } pub fn isBuiltin(self: TypeId) bool { return self.index() < first_user; } }; // ── TypeInfo ──────────────────────────────────────────────────────────── // Resolved type information stored in the TypeTable. // Unlike the AST-level `types.Type` which uses string names for references, // TypeInfo uses TypeId handles, making it fully resolved and internable. pub const TypeInfo = union(enum) { signed: u8, // bit width: 1–64 unsigned: u8, f32, f64, void, bool, string, // [:0]u8 — fat pointer {ptr, len} @"struct": StructInfo, @"enum": EnumInfo, @"union": UnionInfo, tagged_union: TaggedUnionInfo, array: ArrayInfo, slice: SliceInfo, pointer: PointerInfo, many_pointer: ManyPointerInfo, vector: VectorInfo, function: FunctionInfo, closure: ClosureInfo, optional: OptionalInfo, tuple: TupleInfo, pack: PackInfo, any, protocol: ProtocolInfo, error_set: ErrorSetInfo, noreturn, usize, isize, /// Resolution-failure sentinel (see `TypeId.unresolved`). unresolved, pub const StructInfo = struct { name: StringId, fields: []const Field, // True iff this struct backs a protocol value (registered via // `registerProtocolDecl`). Used by the optional code path: a // `?Protocol` is sentinel-shaped (the protocol struct itself, // null ctx == none) rather than the standard `{T, i1}` discriminated // layout — matching how `?Closure` works. is_protocol: bool = false, // Stable nominal identity, assigned once per decl pointer. Folds into // the intern key so two same-display-name authors get distinct TypeIds. // `0` == structural: the type is keyed by display name alone (legacy). nominal_id: u32 = 0, pub const Field = struct { name: StringId, ty: TypeId, }; }; pub const EnumInfo = struct { name: StringId, variants: []const StringId, is_flags: bool = false, explicit_values: ?[]const i64 = null, // for flags (power-of-2) or custom values backing_type: ?TypeId = null, // e.g. u32 for `enum u32 { ... }` nominal_id: u32 = 0, // stable nominal identity; 0 == structural (legacy) }; pub const UnionInfo = struct { name: StringId, fields: []const StructInfo.Field, nominal_id: u32 = 0, // stable nominal identity; 0 == structural (legacy) }; pub const TaggedUnionInfo = struct { name: StringId, fields: []const StructInfo.Field, tag_type: TypeId, // tag integer type (e.g. .u32, .s64) backing_type: ?TypeId = null, // enum struct backing (e.g. { tag: u32; _: u32; payload: [30]u32; }) explicit_tag_values: ?[]const i64 = null, // explicit variant values (e.g., quit :: 0x100) nominal_id: u32 = 0, // stable nominal identity; 0 == structural (legacy) }; pub const ArrayInfo = struct { element: TypeId, length: u32, }; pub const SliceInfo = struct { element: TypeId, }; pub const PointerInfo = struct { pointee: TypeId, }; pub const ManyPointerInfo = struct { element: TypeId, }; pub const VectorInfo = struct { element: TypeId, length: u32, }; pub const FunctionInfo = struct { params: []const TypeId, ret: TypeId, call_conv: CallConv = .default, /// Pack-variadic shape marker. When set, the signature represents a /// heterogeneous type pack: `params` is the fixed prefix, and a /// per-call-site type list binds the remainder. `pack_start == 0` /// with `params.len == 0` denotes `fn(..$args)`. pack_start: ?u32 = null, }; pub const CallConv = enum { default, c }; pub const ClosureInfo = struct { params: []const TypeId, ret: TypeId, /// Pack-variadic shape marker — same semantics as FunctionInfo. /// `Closure(..$args) -> $R` => params = [], pack_start = 0. pack_start: ?u32 = null, }; pub const OptionalInfo = struct { child: TypeId, }; pub const TupleInfo = struct { fields: []const TypeId, names: ?[]const StringId, }; /// A heterogeneous variadic pack as a first-class type-system value: an /// ordered sequence of per-position element types. Comptime-only — a pack /// lowers to flat positional args before codegen and has NO runtime layout /// (sizeOf panics). `elements.len == 0` is a valid empty pack. pub const PackInfo = struct { elements: []const TypeId, }; pub const ProtocolInfo = struct { name: StringId, methods: []const Method, pub const Method = struct { name: StringId, sig: TypeId, // function type }; }; /// A declared error set `Foo :: error { A, B }`. `tags` are GLOBAL tag /// ids from the TypeTable's `TagRegistry` (sorted, canonical). Identity is /// the `name` (like an enum). Runtime layout is u32 — the error channel's /// tag value; id 0 is reserved for "no error". pub const ErrorSetInfo = struct { name: StringId, tags: []const u32, // sorted global tag ids nominal_id: u32 = 0, // stable nominal identity; 0 == structural (legacy) }; }; // ── StringId ──────────────────────────────────────────────────────────── pub const StringId = enum(u32) { empty = 0, _, pub fn index(self: StringId) u32 { return @intFromEnum(self); } }; // ── StringPool ────────────────────────────────────────────────────────── // Intern strings for type/field/variant names. Deduplicates by content. pub const StringPool = struct { /// Maps string content → StringId for dedup. Keys point to owned allocations in `strings`. map: std.StringHashMap(StringId), /// Owned string data indexed by StringId. Each entry is separately heap-allocated. strings: std.ArrayList([]const u8), next_id: u32, pub fn init(alloc: Allocator) StringPool { var pool = StringPool{ .map = std.StringHashMap(StringId).init(alloc), .strings = std.ArrayList([]const u8).empty, .next_id = 1, // 0 is reserved for empty }; // Slot 0 = empty string (not heap-allocated) pool.strings.append(alloc, "") catch unreachable; return pool; } pub fn deinit(self: *StringPool, alloc: Allocator) void { // Free heap-allocated strings (skip slot 0 which is a string literal) for (self.strings.items[1..]) |s| { alloc.free(@constCast(s)); } self.strings.deinit(alloc); self.map.deinit(); } pub fn intern(self: *StringPool, alloc: Allocator, str: []const u8) StringId { if (str.len == 0) return .empty; if (self.map.get(str)) |id| return id; const id: StringId = @enumFromInt(self.next_id); self.next_id += 1; // Allocate a stable copy — used as both map key and lookup value const owned = alloc.dupe(u8, str) catch unreachable; self.strings.append(alloc, owned) catch unreachable; self.map.put(owned, id) catch unreachable; return id; } pub fn get(self: *const StringPool, id: StringId) []const u8 { const idx = id.index(); if (idx >= self.strings.items.len) return ""; return self.strings.items[idx]; } }; // ── TagRegistry ───────────────────────────────────────────────────────── // Global error-tag pool: tag name → u32 id, monotonic, id 0 reserved for // "no error". Tag identity is the name, program-wide — two declared sets that // list the same tag share its id (the design's global-flat tag identity). A // separate namespace from StringPool so tag ids stay dense (compact id→name // table for `{}` interpolation + traces). pub const TagRegistry = struct { /// tag name → id. Keys point to owned allocations in `names`. map: std.StringHashMap(u32), /// id → tag name. Index 0 is the reserved "" (no-error) slot. names: std.ArrayList([]const u8), next_id: u32, pub fn init(alloc: Allocator) TagRegistry { var reg = TagRegistry{ .map = std.StringHashMap(u32).init(alloc), .names = std.ArrayList([]const u8).empty, .next_id = 1, // 0 reserved for "no error" }; reg.names.append(alloc, "") catch unreachable; // slot 0 return reg; } pub fn deinit(self: *TagRegistry, alloc: Allocator) void { for (self.names.items[1..]) |n| alloc.free(@constCast(n)); self.names.deinit(alloc); self.map.deinit(); } pub fn intern(self: *TagRegistry, alloc: Allocator, name: []const u8) u32 { if (self.map.get(name)) |id| return id; const id = self.next_id; self.next_id += 1; const owned = alloc.dupe(u8, name) catch unreachable; self.names.append(alloc, owned) catch unreachable; self.map.put(owned, id) catch unreachable; return id; } pub fn getName(self: *const TagRegistry, id: u32) []const u8 { if (id >= self.names.items.len) return ""; return self.names.items[id]; } }; // ── TypeTable ─────────────────────────────────────────────────────────── // Holds all resolved types. Builtins in slots 0–15, user types interned from 16+. pub const TypeTable = struct { infos: std.ArrayList(TypeInfo), strings: StringPool, /// Global error-tag pool (string → u32 id). Populated as `error { ... }` /// sets are registered; queried when lowering `error.X` value expressions. tags: TagRegistry, /// Maps TypeInfo → TypeId for dedup of structural types intern_map: std.HashMap(TypeKey, TypeId, TypeKeyContext, 80), /// Stable nominal identity: the declaring decl's pointer → its TypeId. The /// `fn_decl_fids` analogue — one entry per declaring decl, so two /// same-display-name declarations resolve to distinct TypeIds via their own /// decl pointer. Keyed by the opaque `RawDeclRef` inner pointer (e.g. /// `*const ast.StructDecl`) — the SAME pointer the import raw-facts hold and /// `registerStructDecl` receives, so registration and resolution agree on /// identity without threading the wrapping `ast.Node`. Populated by the /// resolver (E2) as it assigns nominal ids. type_decl_tids: std.AutoHashMap(*const anyopaque, TypeId), alloc: Allocator, /// Owns the element/param slices duped by the type constructors /// (`functionType*`, `closureType*`, `packType`). Freed wholesale in /// `deinit` — these slices live as long as the table, so an arena avoids /// per-slice bookkeeping and the owned-vs-borrowed ambiguity that blocks /// freeing them individually. slice_arena: std.heap.ArenaAllocator, /// Target pointer size in bytes (4 for wasm32, 8 for 64-bit targets). pointer_size: u8 = 8, pub fn init(alloc: Allocator) TypeTable { var table = TypeTable{ .infos = std.ArrayList(TypeInfo).empty, .strings = StringPool.init(alloc), .tags = TagRegistry.init(alloc), .intern_map = std.HashMap(TypeKey, TypeId, TypeKeyContext, 80).init(alloc), .type_decl_tids = std.AutoHashMap(*const anyopaque, TypeId).init(alloc), .alloc = alloc, .slice_arena = std.heap.ArenaAllocator.init(alloc), }; // Pre-populate builtin slots 0–17 (must match TypeId enum order) const builtins = [_]TypeInfo{ .unresolved, // 0: resolution-failure sentinel .bool, // 1 .{ .signed = 8 }, // 2: s8 .{ .signed = 16 }, // 3: s16 .{ .signed = 32 }, // 4: s32 .{ .signed = 64 }, // 5: s64 .{ .unsigned = 8 }, // 6: u8 .{ .unsigned = 16 }, // 7: u16 .{ .unsigned = 32 }, // 8: u32 .{ .unsigned = 64 }, // 9: u64 .f32, // 10 .f64, // 11 .string, // 12 .any, // 13 .noreturn, // 14 .isize, // 15: isize (pointer-sized signed) .usize, // 16: usize (pointer-sized unsigned) .void, // 17 }; for (&builtins) |info| { table.infos.append(alloc, info) catch unreachable; } return table; } pub fn deinit(self: *TypeTable) void { self.infos.deinit(self.alloc); self.strings.deinit(self.alloc); self.tags.deinit(self.alloc); self.intern_map.deinit(); self.type_decl_tids.deinit(); self.slice_arena.deinit(); } /// Look up the TypeInfo for a given TypeId. pub fn get(self: *const TypeTable, id: TypeId) TypeInfo { return self.infos.items[id.index()]; } /// Intern a TypeInfo, returning the existing TypeId if structurally equal. pub fn intern(self: *TypeTable, info: TypeInfo) TypeId { const key = TypeKey{ .info = info }; if (self.intern_map.get(key)) |existing| { return existing; } const id = TypeId.fromIndex(@intCast(self.infos.items.len)); self.infos.append(self.alloc, info) catch unreachable; self.intern_map.putNoClobber(key, id) catch unreachable; return id; } /// Intern a nominal type (struct/enum/union/tagged_union/error_set) under a /// stable nominal identity. `nominal_id` folds into the intern key so two /// authors that share a display name still get distinct TypeIds. With /// `nominal_id == 0` this is byte-identical to `intern` (structural keying), /// which is the only id used until same-name shadows land. Passing a nonzero /// id for a non-nominal info is a caller bug (the id would be dropped). pub fn internNominal(self: *TypeTable, info: TypeInfo, nominal_id: u32) TypeId { var stamped = info; switch (stamped) { .@"struct" => |*s| s.nominal_id = nominal_id, .@"enum" => |*e| e.nominal_id = nominal_id, .@"union" => |*u| u.nominal_id = nominal_id, .tagged_union => |*u| u.nominal_id = nominal_id, .error_set => |*e| e.nominal_id = nominal_id, else => std.debug.assert(nominal_id == 0), } return self.intern(stamped); } /// Replace the TypeInfo for an existing TypeId WITHOUT changing its intern /// key. Used when a forward-declared type (struct with empty fields) gets /// its full definition later: the key is the display name + nominal id, and /// a field-fill touches neither. Asserts the key is unchanged so a real /// re-key can't sneak through this path (use `replaceKeyedInfo` for that). pub fn updatePreservingKey(self: *TypeTable, id: TypeId, info: TypeInfo) void { const idx = id.index(); const old = self.infos.items[idx]; std.debug.assert((TypeKeyContext{}).eql(.{ .info = old }, .{ .info = info })); self.infos.items[idx] = info; } /// Replace the TypeInfo for an existing TypeId AND re-key `intern_map` to /// match the new info. The one legitimate re-key is the anonymous-type /// rename (`__anon` → `Parent.field`), which mutates the display name and /// therefore the key. Removes the stale key and installs the new one so the /// renamed type interns and looks up under its new name only. pub fn replaceKeyedInfo(self: *TypeTable, id: TypeId, info: TypeInfo) void { const idx = id.index(); const old = self.infos.items[idx]; _ = self.intern_map.remove(.{ .info = old }); self.infos.items[idx] = info; self.intern_map.put(.{ .info = info }, id) catch unreachable; } /// Find a named type (struct/union/enum) by its StringId name. /// Returns the TypeId if found, null otherwise. pub fn findByName(self: *const TypeTable, name: StringId) ?TypeId { for (self.infos.items, 0..) |info, i| { const n: ?StringId = switch (info) { .@"struct" => |s| s.name, .@"union" => |u| u.name, .tagged_union => |u| u.name, .@"enum" => |e| e.name, .error_set => |e| e.name, else => null, }; if (n != null and n.? == name) return TypeId.fromIndex(@intCast(i)); } return null; } /// Source-sensitive variant of `findByName`: asserts at most one named type /// matches, then returns it (or null). Quarantines the global first-match /// scan — new resolver code that must not silently pick a first-of-many /// author uses this so a same-name collision trips the assert instead of /// resolving arbitrarily. pub fn findUniqueByName(self: *const TypeTable, name: StringId) ?TypeId { var found: ?TypeId = null; for (self.infos.items, 0..) |info, i| { const n: ?StringId = switch (info) { .@"struct" => |s| s.name, .@"union" => |u| u.name, .tagged_union => |u| u.name, .@"enum" => |e| e.name, .error_set => |e| e.name, else => null, }; if (n != null and n.? == name) { std.debug.assert(found == null); found = TypeId.fromIndex(@intCast(i)); } } return found; } // ── Convenience constructors ──────────────────────────────────────── pub fn ptrTo(self: *TypeTable, pointee: TypeId) TypeId { return self.intern(.{ .pointer = .{ .pointee = pointee } }); } pub fn manyPtrTo(self: *TypeTable, element: TypeId) TypeId { return self.intern(.{ .many_pointer = .{ .element = element } }); } pub fn sliceOf(self: *TypeTable, element: TypeId) TypeId { return self.intern(.{ .slice = .{ .element = element } }); } pub fn arrayOf(self: *TypeTable, element: TypeId, length: u32) TypeId { return self.intern(.{ .array = .{ .element = element, .length = length } }); } pub fn optionalOf(self: *TypeTable, child: TypeId) TypeId { return self.intern(.{ .optional = .{ .child = child } }); } pub fn functionType(self: *TypeTable, params: []const TypeId, ret: TypeId) TypeId { return self.functionTypeCC(params, ret, .default); } pub fn functionTypeCC(self: *TypeTable, params: []const TypeId, ret: TypeId, cc: TypeInfo.CallConv) TypeId { const owned_params = self.slice_arena.allocator().dupe(TypeId, params) catch unreachable; return self.intern(.{ .function = .{ .params = owned_params, .ret = ret, .call_conv = cc } }); } pub fn functionTypePack(self: *TypeTable, params: []const TypeId, ret: TypeId, cc: TypeInfo.CallConv, pack_start: u32) TypeId { const owned_params = self.slice_arena.allocator().dupe(TypeId, params) catch unreachable; return self.intern(.{ .function = .{ .params = owned_params, .ret = ret, .call_conv = cc, .pack_start = pack_start } }); } pub fn closureType(self: *TypeTable, params: []const TypeId, ret: TypeId) TypeId { const owned_params = self.slice_arena.allocator().dupe(TypeId, params) catch unreachable; return self.intern(.{ .closure = .{ .params = owned_params, .ret = ret } }); } pub fn closureTypePack(self: *TypeTable, params: []const TypeId, ret: TypeId, pack_start: u32) TypeId { const owned_params = self.slice_arena.allocator().dupe(TypeId, params) catch unreachable; return self.intern(.{ .closure = .{ .params = owned_params, .ret = ret, .pack_start = pack_start } }); } pub fn vectorOf(self: *TypeTable, element: TypeId, length: u32) TypeId { return self.intern(.{ .vector = .{ .element = element, .length = length } }); } /// Construct (and intern) a heterogeneous pack type from an ordered /// element-type list. `elements.len == 0` yields the empty pack. pub fn packType(self: *TypeTable, elements: []const TypeId) TypeId { const owned = self.slice_arena.allocator().dupe(TypeId, elements) catch unreachable; return self.intern(.{ .pack = .{ .elements = owned } }); } /// Intern an error-tag name into the global tag pool, returning its id. pub fn internTag(self: *TypeTable, name: []const u8) u32 { return self.tags.intern(self.alloc, name); } /// Look up a tag name from its global id. pub fn getTagName(self: *const TypeTable, id: u32) []const u8 { return self.tags.getName(id); } /// Construct (and intern) a named error-set type. `tag_ids` are global tag /// ids (from `internTag`); they are sorted here for canonical storage. pub fn errorSetType(self: *TypeTable, name: StringId, tag_ids: []const u32) TypeId { const owned = self.slice_arena.allocator().dupe(u32, tag_ids) catch unreachable; std.mem.sort(u32, owned, {}, std.sort.asc(u32)); return self.intern(.{ .error_set = .{ .name = name, .tags = owned } }); } /// Size in bytes for a type (pointer-sized = 8 on 64-bit). pub fn sizeOf(self: *const TypeTable, id: TypeId) u32 { const info = self.get(id); return switch (info) { .void, .noreturn => 0, .bool => 1, .signed => |w| @max(1, w / 8), .unsigned => |w| @max(1, w / 8), .f32 => 4, .f64 => 8, .string => 16, // {ptr, len} .pointer, .many_pointer, .function => 8, .closure => 16, // {fn_ptr, env} .optional => |opt| blk: { // Sentinel-shaped optionals (pointer/closure/protocol) cost // no extra storage — null reuses the payload's null state. const child_info = self.get(opt.child); if (child_info == .pointer or child_info == .many_pointer or child_info == .function) break :blk 8; if (child_info == .closure) break :blk 16; if (child_info == .@"struct" and child_info.@"struct".is_protocol) break :blk self.sizeOf(opt.child); // Discriminated form: payload + has_value flag (8-aligned). break :blk self.sizeOf(opt.child) + 8; }, .slice => 16, // {ptr, len} .array => |arr| arr.length * self.sizeOf(arr.element), .vector => |vec| vec.length * self.sizeOf(vec.element), .any => 16, // {type_tag, data_ptr} .@"struct" => |s| { var total: u32 = 0; for (s.fields) |f| total += @max(self.sizeOf(f.ty), 8); return if (total == 0) 8 else total; }, .@"union" => |u| { var max_field: u32 = 0; for (u.fields) |f| { const sz = self.sizeOf(f.ty); if (sz > max_field) max_field = sz; } return @max(max_field, 8); }, .tagged_union => |u| { if (u.backing_type) |bt| return self.sizeOf(bt); var max_field: u32 = 0; for (u.fields) |f| { const sz = self.sizeOf(f.ty); if (sz > max_field) max_field = sz; } const tag_sz = @as(u32, @intCast(self.typeSizeBytes(u.tag_type))); return tag_sz + @max(max_field, 8); }, .@"enum" => |e| { if (e.backing_type) |bt| return self.sizeOf(bt); return 8; }, .tuple => |t| { var total: u32 = 0; for (t.fields) |f| total += @max(self.sizeOf(f), 8); return if (total == 0) 8 else total; }, .protocol => 16, // {ctx, vtable} .error_set => 4, // u32 tag id on the error channel .usize, .isize => 8, // pointer-sized (this path is not target-aware; see typeSizeBytes) // Comptime-only: a pack must be expanded to flat positional args // before codegen. Reaching runtime layout means a pack leaked. .pack => @panic("pack type has no runtime layout (comptime-only)"), // Tripwire: a failed type resolution must have surfaced a // diagnostic and aborted before any layout query. .unresolved => @panic("unresolved type reached sizeOf — a type resolution failure was not diagnosed/aborted"), }; } /// Compute the ABI size in bytes for a type, matching LLVM's struct layout rules. /// This is the authoritative size computation used for closure env sizing and /// verified against LLVMABISizeOfType. fn intAbiBytes(w: u16) usize { // LLVM ABI size for iN: round w up to the next power of 2, then /8. // Sub-byte widths (i1, i2, ..., i7) are 1 byte. if (w <= 8) return 1; if (w <= 16) return 2; if (w <= 32) return 4; return 8; } /// True iff `ty` is an unsigned integer — a builtin (u8/u16/u32/u64/usize) /// or a user-defined arbitrary-width unsigned int. Canonical signedness /// query for reflection (`type_is_unsigned`) and the `{}` formatter so a /// u64 value renders as unsigned decimal rather than the s64 reinterpretation. pub fn isUnsignedInt(self: *const TypeTable, ty: TypeId) bool { switch (ty) { .u8, .u16, .u32, .u64, .usize => return true, .bool, .s8, .s16, .s32, .s64, .isize => return false, else => {}, } if (ty.isBuiltin()) return false; return self.get(ty) == .unsigned; } pub fn typeSizeBytes(self: *const TypeTable, ty: TypeId) usize { const ptr_size: usize = self.pointer_size; if (ty == .void) return 0; if (ty == .bool) return 1; if (ty == .u8 or ty == .s8) return 1; if (ty == .u16 or ty == .s16) return 2; if (ty == .s32 or ty == .u32 or ty == .f32) return 4; if (ty == .s64 or ty == .u64 or ty == .f64) return 8; if (ty == .usize or ty == .isize) return ptr_size; if (ty == .string) return 16; // {ptr, i64} — always 16 (i64 alignment pads on wasm32) if (ty == .any) return 16; // {i64 tag, i64 value} — Any boxed layout if (ty.isBuiltin()) return ptr_size; // default for unknown builtins const info = self.get(ty); return switch (info) { .pointer, .many_pointer, .function => ptr_size, .slice => 16, // {ptr, i64} — same layout as string .closure => 2 * ptr_size, // {fn_ptr, env_ptr} .optional => |o| blk: { const child_info = self.get(o.child); if (child_info == .pointer or child_info == .many_pointer or child_info == .function) break :blk ptr_size; if (child_info == .closure) break :blk 2 * ptr_size; if (child_info == .@"struct" and child_info.@"struct".is_protocol) break :blk self.typeSizeBytes(o.child); const cs = self.typeSizeBytes(o.child); const ca = self.typeAlignBytes(o.child); // { T, i1 } — i1 goes right after T, then pad to struct alignment const unpadded = cs + 1; break :blk (unpadded + ca - 1) & ~(ca - 1); }, .@"struct" => |s| blk: { var offset: usize = 0; var max_a: usize = 1; for (s.fields) |f| { const fs = self.typeSizeBytes(f.ty); const fa = self.typeAlignBytes(f.ty); if (fa > max_a) max_a = fa; offset = (offset + fa - 1) & ~(fa - 1); offset += fs; } break :blk if (offset == 0) 0 else (offset + max_a - 1) & ~(max_a - 1); }, .@"union" => |u| blk: { var max_payload: usize = 0; for (u.fields) |f| { const fs = self.typeSizeBytes(f.ty); if (fs > max_payload) max_payload = fs; } break :blk if (max_payload == 0) 8 else max_payload; }, .tagged_union => |u| blk: { if (u.backing_type) |bt| break :blk self.typeSizeBytes(bt); var max_payload: usize = 0; for (u.fields) |f| { const fs = self.typeSizeBytes(f.ty); if (fs > max_payload) max_payload = fs; } const tag_size = self.typeSizeBytes(u.tag_type); const raw = max_payload + tag_size; break :blk (raw + 7) & ~@as(usize, 7); }, .array => |a| blk: { const elem_size = self.typeSizeBytes(a.element); break :blk elem_size * @as(usize, @intCast(a.length)); }, .vector => |v| blk: { const elem_size = self.typeSizeBytes(v.element); const raw = elem_size * @as(usize, @intCast(v.length)); // LLVM vectors round ABI size up to next power of 2 break :blk std.math.ceilPowerOfTwo(usize, raw) catch raw; }, .tuple => |t| blk: { var offset: usize = 0; var max_a: usize = 1; for (t.fields) |f| { const fs = self.typeSizeBytes(f); const fa = self.typeAlignBytes(f); if (fa > max_a) max_a = fa; offset = (offset + fa - 1) & ~(fa - 1); offset += fs; } break :blk if (offset == 0) 0 else (offset + max_a - 1) & ~(max_a - 1); }, .any => 2 * ptr_size, // {type_tag, data_ptr} .protocol => 2 * ptr_size, // {ctx, vtable} .error_set => 4, // u32 tag id .@"enum" => |e| { if (e.backing_type) |bt| return self.typeSizeBytes(bt); return 8; }, // LLVM rounds arbitrary-width integers up to the next power-of-2 // width before computing ABI size (i12 → 2 bytes, i24 → 4 bytes). .signed => |w| intAbiBytes(w), .unsigned => |w| intAbiBytes(w), else => 8, }; } /// Compute the ABI alignment in bytes for a type, matching LLVM's rules. pub fn typeAlignBytes(self: *const TypeTable, ty: TypeId) usize { const ptr_align: usize = self.pointer_size; if (ty == .void) return 1; if (ty == .bool) return 1; if (ty == .u8 or ty == .s8) return 1; if (ty == .u16 or ty == .s16) return 2; if (ty == .s32 or ty == .u32 or ty == .f32) return 4; if (ty == .s64 or ty == .u64 or ty == .f64) return 8; if (ty == .usize or ty == .isize) return ptr_align; if (ty == .string) return 8; // i64 drives alignment if (ty == .any) return 8; // {i64, i64} aligns to 8 if (ty.isBuiltin()) return ptr_align; const info = self.get(ty); return switch (info) { .pointer, .many_pointer, .function => ptr_align, .slice => 8, // i64 drives alignment .closure => ptr_align, // {ptr, ptr} .optional => |o| blk: { const child_info = self.get(o.child); if (child_info == .pointer or child_info == .many_pointer or child_info == .function or child_info == .closure) break :blk ptr_align; break :blk self.typeAlignBytes(o.child); }, .@"struct" => |s| blk: { var max_a: usize = 1; for (s.fields) |f| { const fa = self.typeAlignBytes(f.ty); if (fa > max_a) max_a = fa; } break :blk max_a; }, .@"union", .tagged_union => 8, .error_set => 4, // u32 tag id .@"enum" => |e| { if (e.backing_type) |bt| return self.typeAlignBytes(bt); return 8; }, .array => |a| self.typeAlignBytes(a.element), .vector => |v| self.typeAlignBytes(v.element), .tuple => |t| blk: { var max_a: usize = 1; for (t.fields) |f| { const fa = self.typeAlignBytes(f); if (fa > max_a) max_a = fa; } break :blk max_a; }, .signed => |w| intAbiBytes(w), .unsigned => |w| intAbiBytes(w), else => 8, }; } /// Intern a string into the pool. pub fn internString(self: *TypeTable, str: []const u8) StringId { return self.strings.intern(self.alloc, str); } /// Look up a string from its id. pub fn getString(self: *const TypeTable, id: StringId) []const u8 { return self.strings.get(id); } /// Format a TypeId for display (e.g., "s32", "*bool", "[]u8"). pub fn typeName(self: *const TypeTable, id: TypeId) []const u8 { // Fast path for builtins return switch (id) { .void => "void", .bool => "bool", .s8 => "s8", .s16 => "s16", .s32 => "s32", .s64 => "s64", .u8 => "u8", .u16 => "u16", .u32 => "u32", .u64 => "u64", .f32 => "f32", .f64 => "f64", .string => "string", .any => "Any", .noreturn => "noreturn", .isize => "isize", .usize => "usize", .unresolved => "", else => { // User types — format from TypeInfo const info = self.get(id); return switch (info) { .@"struct" => |s| self.getString(s.name), .@"enum" => |e| self.getString(e.name), .@"union" => |u| self.getString(u.name), .tagged_union => |u| self.getString(u.name), .protocol => |p| self.getString(p.name), .error_set => |e| self.getString(e.name), else => "?", }; }, }; } /// Like `typeName` but produces structural names for compound /// types (`*T`, `[]T`, `[N]T`, `?T`, `Vector(N,T)`, function and /// tuple types) instead of returning `"?"`. Compound names are /// freshly allocated via `alloc`; builtin and named user types /// return borrowed slices. pub fn formatTypeName(self: *const TypeTable, alloc: std.mem.Allocator, id: TypeId) []const u8 { if (id.isBuiltin()) return self.typeName(id); const info = self.get(id); return switch (info) { .@"struct" => |s| self.getString(s.name), .@"enum" => |e| self.getString(e.name), .@"union" => |u| self.getString(u.name), .tagged_union => |u| self.getString(u.name), .protocol => |p| self.getString(p.name), .error_set => |e| self.getString(e.name), .pointer => |p| blk: { const inner = self.formatTypeName(alloc, p.pointee); break :blk std.fmt.allocPrint(alloc, "*{s}", .{inner}) catch "*?"; }, .many_pointer => |p| blk: { const inner = self.formatTypeName(alloc, p.element); break :blk std.fmt.allocPrint(alloc, "[*]{s}", .{inner}) catch "[*]?"; }, .slice => |s| blk: { const inner = self.formatTypeName(alloc, s.element); break :blk std.fmt.allocPrint(alloc, "[]{s}", .{inner}) catch "[]?"; }, .array => |a| blk: { const inner = self.formatTypeName(alloc, a.element); break :blk std.fmt.allocPrint(alloc, "[{d}]{s}", .{ a.length, inner }) catch "[N]?"; }, .vector => |v| blk: { const inner = self.formatTypeName(alloc, v.element); break :blk std.fmt.allocPrint(alloc, "Vector({d},{s})", .{ v.length, inner }) catch "Vector(?)"; }, .optional => |o| blk: { const inner = self.formatTypeName(alloc, o.child); break :blk std.fmt.allocPrint(alloc, "?{s}", .{inner}) catch "?_"; }, .function => |f| blk: { var buf = std.ArrayList(u8).empty; defer buf.deinit(alloc); buf.append(alloc, '(') catch break :blk "(?)"; for (f.params, 0..) |p, i| { if (i > 0) buf.appendSlice(alloc, ", ") catch break :blk "(?)"; buf.appendSlice(alloc, self.formatTypeName(alloc, p)) catch break :blk "(?)"; } buf.append(alloc, ')') catch break :blk "(?)"; if (f.ret != .void) { buf.appendSlice(alloc, " -> ") catch break :blk "(?)"; buf.appendSlice(alloc, self.formatTypeName(alloc, f.ret)) catch break :blk "(?)"; } break :blk buf.toOwnedSlice(alloc) catch "(?)"; }, .closure => |co| blk: { var buf = std.ArrayList(u8).empty; defer buf.deinit(alloc); buf.appendSlice(alloc, "Closure(") catch break :blk "Closure(?)"; for (co.params, 0..) |p, i| { if (i > 0) buf.appendSlice(alloc, ", ") catch break :blk "Closure(?)"; buf.appendSlice(alloc, self.formatTypeName(alloc, p)) catch break :blk "Closure(?)"; } buf.append(alloc, ')') catch break :blk "Closure(?)"; if (co.ret != .void) { buf.appendSlice(alloc, " -> ") catch break :blk "Closure(?)"; buf.appendSlice(alloc, self.formatTypeName(alloc, co.ret)) catch break :blk "Closure(?)"; } break :blk buf.toOwnedSlice(alloc) catch "Closure(?)"; }, .tuple => |tu| blk: { var buf = std.ArrayList(u8).empty; defer buf.deinit(alloc); buf.append(alloc, '(') catch break :blk "(?)"; for (tu.fields, 0..) |f, i| { if (i > 0) buf.appendSlice(alloc, ", ") catch break :blk "(?)"; buf.appendSlice(alloc, self.formatTypeName(alloc, f)) catch break :blk "(?)"; } buf.append(alloc, ')') catch break :blk "(?)"; break :blk buf.toOwnedSlice(alloc) catch "(?)"; }, .pack => |pk| blk: { var buf = std.ArrayList(u8).empty; defer buf.deinit(alloc); buf.appendSlice(alloc, "pack(") catch break :blk "pack(?)"; for (pk.elements, 0..) |e, i| { if (i > 0) buf.appendSlice(alloc, ", ") catch break :blk "pack(?)"; buf.appendSlice(alloc, self.formatTypeName(alloc, e)) catch break :blk "pack(?)"; } buf.append(alloc, ')') catch break :blk "pack(?)"; break :blk buf.toOwnedSlice(alloc) catch "pack(?)"; }, .signed => |w| std.fmt.allocPrint(alloc, "s{d}", .{w}) catch "s?", .unsigned => |w| std.fmt.allocPrint(alloc, "u{d}", .{w}) catch "u?", else => self.typeName(id), }; } }; // ── Intern map support ────────────────────────────────────────────────── // We use a custom hash/eql context so structurally identical types dedup. const TypeKey = struct { info: TypeInfo, }; const TypeKeyContext = struct { pub fn hash(_: TypeKeyContext, key: TypeKey) u64 { var h = std.hash.Wyhash.init(0); hashTypeInfo(&h, key.info); return h.final(); } pub fn eql(_: TypeKeyContext, a: TypeKey, b: TypeKey) bool { return typeInfoEql(a.info, b.info); } }; fn hashTypeInfo(h: *std.hash.Wyhash, info: TypeInfo) void { // Hash the tag const tag: u8 = @intFromEnum(std.meta.activeTag(info)); h.update(&.{tag}); switch (info) { .signed => |w| h.update(&.{w}), .unsigned => |w| h.update(&.{w}), .f32, .f64, .void, .bool, .string, .any, .noreturn, .usize, .isize, .unresolved => {}, .pointer => |p| h.update(std.mem.asBytes(&p.pointee)), .many_pointer => |p| h.update(std.mem.asBytes(&p.element)), .slice => |s| h.update(std.mem.asBytes(&s.element)), .array => |a| { h.update(std.mem.asBytes(&a.element)); h.update(std.mem.asBytes(&a.length)); }, .vector => |v| { h.update(std.mem.asBytes(&v.element)); h.update(std.mem.asBytes(&v.length)); }, .optional => |o| h.update(std.mem.asBytes(&o.child)), .function => |f| { for (f.params) |p| h.update(std.mem.asBytes(&p)); h.update(std.mem.asBytes(&f.ret)); const cc_byte: u8 = @intFromEnum(f.call_conv); h.update(&.{cc_byte}); const pack_present: u8 = if (f.pack_start != null) 1 else 0; h.update(&.{pack_present}); if (f.pack_start) |ps| h.update(std.mem.asBytes(&ps)); }, .closure => |c| { for (c.params) |p| h.update(std.mem.asBytes(&p)); h.update(std.mem.asBytes(&c.ret)); const pack_present: u8 = if (c.pack_start != null) 1 else 0; h.update(&.{pack_present}); if (c.pack_start) |ps| h.update(std.mem.asBytes(&ps)); }, // Nominal arms key by display name; `nominal_id` joins the key only when // nonzero, so structural (legacy) interning hashes byte-identically. .@"struct" => |s| { h.update(std.mem.asBytes(&s.name)); if (s.nominal_id != 0) h.update(std.mem.asBytes(&s.nominal_id)); }, .@"enum" => |e| { h.update(std.mem.asBytes(&e.name)); if (e.nominal_id != 0) h.update(std.mem.asBytes(&e.nominal_id)); }, .@"union" => |u| { h.update(std.mem.asBytes(&u.name)); if (u.nominal_id != 0) h.update(std.mem.asBytes(&u.nominal_id)); }, .tagged_union => |u| { h.update(std.mem.asBytes(&u.name)); if (u.nominal_id != 0) h.update(std.mem.asBytes(&u.nominal_id)); }, .protocol => |p| h.update(std.mem.asBytes(&p.name)), .error_set => |e| { h.update(std.mem.asBytes(&e.name)); if (e.nominal_id != 0) h.update(std.mem.asBytes(&e.nominal_id)); }, .tuple => |t| { for (t.fields) |f| h.update(std.mem.asBytes(&f)); if (t.names) |ns| for (ns) |n| h.update(std.mem.asBytes(&n)); }, .pack => |p| { for (p.elements) |e| h.update(std.mem.asBytes(&e)); }, } } fn typeInfoEql(a: TypeInfo, b: TypeInfo) bool { const Tag = std.meta.Tag(TypeInfo); const a_tag: Tag = a; const b_tag: Tag = b; if (a_tag != b_tag) return false; return switch (a) { .signed => |w| w == b.signed, .unsigned => |w| w == b.unsigned, .f32, .f64, .void, .bool, .string, .any, .noreturn, .usize, .isize, .unresolved => true, .pointer => |p| p.pointee == b.pointer.pointee, .many_pointer => |p| p.element == b.many_pointer.element, .slice => |s| s.element == b.slice.element, .array => |ar| ar.element == b.array.element and ar.length == b.array.length, .vector => |v| v.element == b.vector.element and v.length == b.vector.length, .optional => |o| o.child == b.optional.child, .function => |f| { const g = b.function; if (f.params.len != g.params.len) return false; for (f.params, g.params) |fp, gp| { if (fp != gp) return false; } if (f.call_conv != g.call_conv) return false; if ((f.pack_start == null) != (g.pack_start == null)) return false; if (f.pack_start) |fp| if (fp != g.pack_start.?) return false; return f.ret == g.ret; }, .closure => |c| { const d = b.closure; if (c.params.len != d.params.len) return false; for (c.params, d.params) |cp, dp| { if (cp != dp) return false; } if ((c.pack_start == null) != (d.pack_start == null)) return false; if (c.pack_start) |cp| if (cp != d.pack_start.?) return false; return c.ret == d.ret; }, // Nominal arms compare display name + nominal id. With both ids 0 this is // name-only equality (legacy); a nonzero id distinguishes same-name authors. .@"struct" => |s| s.name == b.@"struct".name and s.nominal_id == b.@"struct".nominal_id, .@"enum" => |e| e.name == b.@"enum".name and e.nominal_id == b.@"enum".nominal_id, .@"union" => |u| u.name == b.@"union".name and u.nominal_id == b.@"union".nominal_id, .tagged_union => |u| u.name == b.tagged_union.name and u.nominal_id == b.tagged_union.nominal_id, .protocol => |p| p.name == b.protocol.name, .error_set => |e| e.name == b.error_set.name and e.nominal_id == b.error_set.nominal_id, .tuple => |t| { const u = b.tuple; if (t.fields.len != u.fields.len) return false; for (t.fields, u.fields) |tf, uf| { if (tf != uf) return false; } if ((t.names == null) != (u.names == null)) return false; if (t.names) |tn| { const un = u.names.?; if (tn.len != un.len) return false; for (tn, un) |tna, una| if (tna != una) return false; } return true; }, .pack => |p| { const q = b.pack; if (p.elements.len != q.elements.len) return false; for (p.elements, q.elements) |pe, qe| { if (pe != qe) return false; } return true; }, }; }