From 9b50aacbe4c7d91c4a4419409175f18df2ca63c0 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 2 Jun 2026 15:20:31 +0300 Subject: [PATCH] refactor(ir): converge structural type-shape resolution onto resolveCompound (A2.3b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex corrective step before the A2 merge gate: A2.3 left type_bridge with a parallel structural type-resolution algorithm and an inline tuple-literal-spread shape in lower.zig with a `.void` fallback. Finding 1 — single owner for structural shapes: - TypeResolver.resolveCompound is now the sole structural type-shape constructor. Namespaced on `table` (so the stateless type_bridge can call it) and extended to own function types, plain `Closure(P...) -> R`, and plain positional/named tuples (it already owned *T/[*]T/[]T/?T/[N]T). It returns null only for the pack-shaped forms that need caller state (`Closure(..p)`, spread tuples); OOM yields `.unresolved`. - type_bridge: deleted its 8 independent structural resolvers (resolveArray/Slice/Pointer/ManyPointer/Optional/Function/Closure/TupleType). resolveAstType delegates those node kinds to resolveCompound via a binding-free StatelessInner adapter. The only residual stateless shape code is two tiny fallbacks for the pack-shaped forms resolveCompound defers (resolveClosurePackShape — used by Into(Block) at registration time — and resolveTupleSpreadShape) plus resolveParameterizedType (kept: generic-instantiation convergence is A4.1 per PLAN-ARCH). - lower.zig: stateful resolveTypeWithBindings uses resolveCompound; the `.function_type_expr` switch arm is gone. PackResolver.resolveFunctionTypeWithBindings deleted (subsumed). Plain closures/tuples now resolve via resolveCompound in both paths; only pack closures / spread tuples reach PackResolver. Finding 2 — no `.void` failure fallback in lower.zig pack handling: - the inline tuple_literal-with-spread type assembly moved into PackResolver.resolveTupleLiteralType (returns ?TypeId; OOM `catch return .void` became `catch return .unresolved`). Alias result preserved: TypeTable.aliases stays gone; no table.aliases reads; ProgramIndex.type_alias_map threaded explicitly. type_resolver.test.zig: resolveCompound test rewritten (namespaced + new function/closure/tuple/pack-shape arms, arena-backed). Gate green: zig build, zig build test, run_examples 350/0. --- src/ir/lower.zig | 53 +++++------------ src/ir/packs.zig | 46 +++++++++------ src/ir/type_bridge.zig | 103 ++++++++++++++++------------------ src/ir/type_resolver.test.zig | 52 ++++++++++++----- src/ir/type_resolver.zig | 81 +++++++++++++++++++++----- 5 files changed, 199 insertions(+), 136 deletions(-) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 54fe156..26f48d4 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -12799,11 +12799,13 @@ pub const Lowering = struct { } } } - // Structural compound types (`*T`, `[*]T`, `[]T`, `?T`, `[N]T`) are - // owned by TypeResolver (A2.1). Element types recurse through the full - // stateful resolver (`resolveInner` → here) so generic structs / - // bindings in element position keep their resolution. - if (self.typeResolver().resolveCompound(node, self)) |t| return t; + // Structural type shapes — `*T`, `[*]T`, `[]T`, `?T`, `[N]T`, functions, + // PLAIN closures, and PLAIN tuples — are owned by + // `TypeResolver.resolveCompound` (A2.3b). Element types recurse through + // the full stateful resolver (`resolveInner` → here) so generic structs + // / bindings keep their resolution. resolveCompound returns null only + // for the pack-shaped forms (`Closure(..p)`, spread tuples) below. + if (TypeResolver.resolveCompound(&self.module.types, node, self)) |t| return t; // Generic type-param binding (`$T`, or a bare return-type `T` without // the `$` prefix) — owned by TypeResolver via the explicit ResolveEnv. // The parameterized / call / closure / function arms that used to live @@ -12817,48 +12819,23 @@ pub const Lowering = struct { if (node.data == .call) { return self.resolveTypeCallWithBindings(&node.data.call); } - // Pointers / slices / many-pointers / optionals / arrays are owned by - // TypeResolver (handled above). The pack-aware tuple / closure / - // function shapes are owned by `PackResolver` (packs.zig, A2.3). + // Plain structural shapes were handled by resolveCompound above. What + // reaches here is the PACK-shaped subset, owned by `PackResolver` + // (packs.zig): pack-shaped `Closure(..p)` and spread tuples. (Functions + // are never pack-shaped at the type level — resolveCompound owns them + // all, so there is no function arm here.) switch (node.data) { .closure_type_expr => |ct| { return self.packResolver().resolveClosureTypeWithBindings(&ct); }, - .function_type_expr => |ft| { - return self.packResolver().resolveFunctionTypeWithBindings(&ft); - }, .tuple_type_expr => |tt| { return self.packResolver().resolveTupleTypeWithBindings(&tt); }, // `(..$Ts)` in a type position (e.g. a struct field) parses as a - // tuple LITERAL whose elements include a pack spread; expand it to - // the bound pack's element types, same as `resolveTupleTypeWithBindings`. + // tuple LITERAL whose elements include a pack spread; PackResolver + // expands it (returns null when no spread, so we fall through). .tuple_literal => |tl| { - var any_spread = false; - for (tl.elements) |el| { - if (el.value.data == .spread_expr) { - any_spread = true; - break; - } - } - if (any_spread) { - var field_ids = std.ArrayList(TypeId).empty; - defer field_ids.deinit(self.alloc); - for (tl.elements) |el| { - if (el.value.data == .spread_expr) { - if (self.packResolver().packTypeElems(el.value.data.spread_expr.operand)) |elems| { - defer self.alloc.free(elems); - for (elems) |e| field_ids.append(self.alloc, e) catch return .void; - continue; - } - } - field_ids.append(self.alloc, self.resolveTypeWithBindings(el.value)) catch return .void; - } - return self.module.types.intern(.{ .tuple = .{ - .fields = self.alloc.dupe(TypeId, field_ids.items) catch return .void, - .names = null, - } }); - } + if (self.packResolver().resolveTupleLiteralType(&tl)) |t| return t; }, else => {}, } diff --git a/src/ir/packs.zig b/src/ir/packs.zig index 92ca150..7d65d96 100644 --- a/src/ir/packs.zig +++ b/src/ir/packs.zig @@ -97,24 +97,36 @@ pub const PackResolver = struct { } }); } - /// Resolve a `(Params...) -> Ret` function type expression with the - /// active type/pack bindings applied. Mirrors - /// `resolveClosureTypeWithBindings` but for `function_type_expr`. - /// Unlocks `$args[$i]` in fn-pointer type literals like - /// `fp : (*void, $args[0]) -> $args[1] = ...` — used in step 5's - /// generic trampoline body. - pub fn resolveFunctionTypeWithBindings(self: PackResolver, ft: *const ast.FunctionTypeExpr) TypeId { - var param_ids = std.ArrayList(TypeId).empty; - defer param_ids.deinit(self.l.alloc); - for (ft.param_types) |pt| { - param_ids.append(self.l.alloc, self.l.resolveTypeWithBindings(pt)) catch return .unresolved; + /// Resolve a tuple LITERAL used in a type position whose elements include a + /// pack spread (`(..$Ts)` / `(..xs.T)` — these parse as a tuple literal, not + /// a `tuple_type_expr`). Returns null when no element is a spread, so the + /// caller falls through to ordinary name/type resolution. A failed + /// allocation yields `.unresolved` (never a real `.void`). + pub fn resolveTupleLiteralType(self: PackResolver, tl: *const ast.TupleLiteral) ?TypeId { + var any_spread = false; + for (tl.elements) |el| { + if (el.value.data == .spread_expr) { + any_spread = true; + break; + } } - const ret_ty = if (ft.return_type) |rt| self.l.resolveTypeWithBindings(rt) else .void; - const cc: types.TypeInfo.CallConv = switch (ft.call_conv) { - .default => .default, - .c => .c, - }; - return self.l.module.types.functionTypeCC(param_ids.items, ret_ty, cc); + if (!any_spread) return null; + var field_ids = std.ArrayList(TypeId).empty; + defer field_ids.deinit(self.l.alloc); + for (tl.elements) |el| { + if (el.value.data == .spread_expr) { + if (self.packTypeElems(el.value.data.spread_expr.operand)) |elems| { + defer self.l.alloc.free(elems); + for (elems) |e| field_ids.append(self.l.alloc, e) catch return .unresolved; + continue; + } + } + field_ids.append(self.l.alloc, self.l.resolveTypeWithBindings(el.value)) catch return .unresolved; + } + return self.l.module.types.intern(.{ .tuple = .{ + .fields = self.l.alloc.dupe(TypeId, field_ids.items) catch return .unresolved, + .names = null, + } }); } /// TYPE-position pack expansion: given a spread operand, return the diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index 8d4c2f8..48d1771 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -18,6 +18,19 @@ const type_resolver = @import("type_resolver.zig"); /// `null` for contexts that never see aliases, e.g. unit tests). pub const AliasMap = ?*const std.StringHashMap(TypeId); +/// Binding-free element-recursion adapter for `TypeResolver.resolveCompound`: +/// nested element types resolve through `type_bridge.resolveAstType` (the +/// registration-time path — no generic/pack bindings). Lets type_bridge reuse +/// the single canonical structural-shape constructor instead of carrying its +/// own compound algorithm (A2.3b). +const StatelessInner = struct { + table: *TypeTable, + alias_map: AliasMap, + pub fn resolveInner(self: StatelessInner, node: *const Node) TypeId { + return resolveAstType(node, self.table, self.alias_map); + } +}; + // ── AST Node → TypeId ─────────────────────────────────────────────────── // Resolve an AST type node into an IR TypeId. Used during lowering when // we only have the parsed AST (no codegen type registry). @@ -29,17 +42,31 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap // `.s64` here would silently fabricate an 8-byte int. Surface it via the // `.unresolved` sentinel (trips the sizeOf/toLLVMType panic at codegen). const n = node orelse return .unresolved; + const si = StatelessInner{ .table = table, .alias_map = alias_map }; return switch (n.data) { .type_expr => |te| resolveTypeName(te.name, table, alias_map), .identifier => |id| resolveTypeName(id.name, table, alias_map), - .array_type_expr => |at| resolveArrayType(&at, table, alias_map), - .slice_type_expr => |st| resolveSliceType(&st, table, alias_map), - .pointer_type_expr => |pt| resolvePointerType(&pt, table, alias_map), - .many_pointer_type_expr => |mpt| resolveManyPointerType(&mpt, table, alias_map), - .optional_type_expr => |ot| resolveOptionalType(&ot, table, alias_map), - .function_type_expr => |ft| resolveFunctionType(&ft, table, alias_map), - .closure_type_expr => |ct| resolveClosureType(&ct, table, alias_map), - .tuple_type_expr => |tt| resolveTupleType(&tt, table, alias_map), + // Structural shapes (`*T`/`[*]T`/`[]T`/`?T`/`[N]T`, functions, plain + // closures, plain tuples) are owned by the single canonical + // `TypeResolver.resolveCompound` — no independent compound algorithm + // lives here (A2.3b). resolveCompound never returns null for these + // kinds, so `.?` is total. + .pointer_type_expr, + .many_pointer_type_expr, + .slice_type_expr, + .optional_type_expr, + .array_type_expr, + .function_type_expr, + => type_resolver.TypeResolver.resolveCompound(table, n, si).?, + // Plain closures/tuples are owned by resolveCompound (above). It returns + // null for the PACK-shaped forms — `Closure(..p)` and spread tuples — + // because expanding a pack needs bindings. type_bridge has none, so it + // preserves the pack SHAPE statelessly (e.g. `Into(Block)` resolves a + // `Closure(..p)` field type at registration time). These tiny fallbacks + // are the only stateless-specific shape code left; the stateful expand + // lives in PackResolver. + .closure_type_expr => |ct| type_resolver.TypeResolver.resolveCompound(table, n, si) orelse resolveClosurePackShape(&ct, table, alias_map), + .tuple_type_expr => |tt| type_resolver.TypeResolver.resolveCompound(table, n, si) orelse resolveTupleSpreadShape(&tt, table, alias_map), .pack_index_type_expr => { // Pack-index `$args[N]` in a type position must be resolved // against an active pack binding — `type_bridge` has no access @@ -208,61 +235,27 @@ fn resolveTypeName(name: []const u8, table: *TypeTable, alias_map: AliasMap) Typ /// retired (A2.2). Single source of truth: the table is defined once, there. pub const resolveTypePrimitive = type_resolver.TypeResolver.resolvePrimitive; -fn resolveArrayType(at: *const ast.ArrayTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId { - const elem = resolveAstType(at.element_type, table, alias_map); - const length: u32 = switch (at.length.data) { - .int_literal => |lit| @intCast(@as(u64, @bitCast(lit.value))), - else => 0, - }; - return table.arrayOf(elem, length); -} - -fn resolveSliceType(st: *const ast.SliceTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId { - const elem = resolveAstType(st.element_type, table, alias_map); - return table.sliceOf(elem); -} - -fn resolvePointerType(pt: *const ast.PointerTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId { - const pointee = resolveAstType(pt.pointee_type, table, alias_map); - return table.ptrTo(pointee); -} - -fn resolveManyPointerType(mpt: *const ast.ManyPointerTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId { - const elem = resolveAstType(mpt.element_type, table, alias_map); - return table.manyPtrTo(elem); -} - -fn resolveOptionalType(ot: *const ast.OptionalTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId { - const child = resolveAstType(ot.inner_type, table, alias_map); - return table.optionalOf(child); -} - -fn resolveFunctionType(ft: *const ast.FunctionTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId { - const alloc = table.alloc; - var param_ids = std.ArrayList(TypeId).empty; - for (ft.param_types) |pt| { - param_ids.append(alloc, resolveAstType(pt, table, alias_map)) catch unreachable; - } - const ret_id = if (ft.return_type) |rt| resolveAstType(rt, table, alias_map) else TypeId.void; - const cc: ir_types.TypeInfo.CallConv = if (ft.call_conv == .c) .c else .default; - return table.functionTypeCC(param_ids.items, ret_id, cc); -} - -fn resolveClosureType(ct: *const ast.ClosureTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId { +/// Pack-shaped `Closure(..p)` resolved without bindings: the canonical +/// `resolveCompound` builds plain closures and defers pack-shaped ones (returns +/// null). type_bridge can't expand the pack (no state), so it preserves the +/// pack SHAPE — a `closureTypePack` whose prefix is the fixed params. The +/// stateful expand lives in `PackResolver.resolveClosureTypeWithBindings`. +fn resolveClosurePackShape(ct: *const ast.ClosureTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId { const alloc = table.alloc; var param_ids = std.ArrayList(TypeId).empty; for (ct.param_types) |pt| { param_ids.append(alloc, resolveAstType(pt, table, alias_map)) catch unreachable; } const ret_id = if (ct.return_type) |rt| resolveAstType(rt, table, alias_map) else TypeId.void; - if (ct.pack_name != null) { - // Pack-variadic shape: fixed prefix in params, pack-start at end. - return table.closureTypePack(param_ids.items, ret_id, @intCast(param_ids.items.len)); - } - return table.closureType(param_ids.items, ret_id); + return table.closureTypePack(param_ids.items, ret_id, @intCast(param_ids.items.len)); } -fn resolveTupleType(tt: *const ast.TupleTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId { +/// Spread tuple `(..xs)` resolved without bindings: `resolveCompound` builds +/// plain tuples and defers spread ones. type_bridge can't expand the pack, so +/// each field resolves individually (a spread field is not a type → resolves to +/// `.unresolved`). The stateful expand lives in +/// `PackResolver.resolveTupleTypeWithBindings`. +fn resolveTupleSpreadShape(tt: *const ast.TupleTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId { const alloc = table.alloc; var field_ids = std.ArrayList(TypeId).empty; for (tt.field_types) |ft| { diff --git a/src/ir/type_resolver.test.zig b/src/ir/type_resolver.test.zig index 3d91de2..e16562f 100644 --- a/src/ir/type_resolver.test.zig +++ b/src/ir/type_resolver.test.zig @@ -42,12 +42,12 @@ test "TypeResolver.resolvePrimitive maps builtin keywords, null otherwise" { } test "TypeResolver.resolveCompound builds structural compound types" { - const alloc = std.testing.allocator; + // Arena-backed: interned tuple field slices are owned by the type table and + // reclaimed in bulk by the real compiler's arena (never freed individually). + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); var table = TypeTable.init(alloc); - defer table.deinit(); - var index = ProgramIndex.init(alloc); - defer index.deinit(); - const tr = TypeResolver{ .alloc = alloc, .types = &table, .diagnostics = null, .index = &index }; const inner = PrimInner{}; var s64n = typeExpr("s64"); @@ -57,25 +57,51 @@ test "TypeResolver.resolveCompound builds structural compound types" { var s32n = typeExpr("s32"); var ptr = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .pointer_type_expr = .{ .pointee_type = &s64n } } }; - try std.testing.expectEqual(@as(?TypeId, table.ptrTo(.s64)), tr.resolveCompound(&ptr, inner)); + try std.testing.expectEqual(@as(?TypeId, table.ptrTo(.s64)), TypeResolver.resolveCompound(&table, &ptr, inner)); var mptr = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .many_pointer_type_expr = .{ .element_type = &u8n } } }; - try std.testing.expectEqual(@as(?TypeId, table.manyPtrTo(.u8)), tr.resolveCompound(&mptr, inner)); + try std.testing.expectEqual(@as(?TypeId, table.manyPtrTo(.u8)), TypeResolver.resolveCompound(&table, &mptr, inner)); var slice = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .slice_type_expr = .{ .element_type = &f32n } } }; - try std.testing.expectEqual(@as(?TypeId, table.sliceOf(.f32)), tr.resolveCompound(&slice, inner)); + try std.testing.expectEqual(@as(?TypeId, table.sliceOf(.f32)), TypeResolver.resolveCompound(&table, &slice, inner)); var opt = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .optional_type_expr = .{ .inner_type = &booln } } }; - try std.testing.expectEqual(@as(?TypeId, table.optionalOf(.bool)), tr.resolveCompound(&opt, inner)); + try std.testing.expectEqual(@as(?TypeId, table.optionalOf(.bool)), TypeResolver.resolveCompound(&table, &opt, inner)); var len = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .int_literal = .{ .value = 3 } } }; var arr = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .array_type_expr = .{ .length = &len, .element_type = &s32n } } }; - try std.testing.expectEqual(@as(?TypeId, table.arrayOf(.s32, 3)), tr.resolveCompound(&arr, inner)); + try std.testing.expectEqual(@as(?TypeId, table.arrayOf(.s32, 3)), TypeResolver.resolveCompound(&table, &arr, inner)); - // Non-compound nodes are not this resolver's responsibility → null, so the - // caller continues with name / tuple / closure / generic resolution. + // Function type `(s64) -> bool` — resolveCompound owns it (A2.3b). + const fparams = [_]*Node{&s64n}; + var fnode = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .function_type_expr = .{ .param_types = &fparams, .return_type = &booln } } }; + try std.testing.expectEqual(@as(?TypeId, table.functionTypeCC(&[_]TypeId{.s64}, .bool, .default)), TypeResolver.resolveCompound(&table, &fnode, inner)); + + // Plain closure `Closure(s64) -> bool` (no pack) — owned here. + const cparams = [_]*Node{&s64n}; + var cnode = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .closure_type_expr = .{ .param_types = &cparams, .return_type = &booln } } }; + try std.testing.expectEqual(@as(?TypeId, table.closureType(&[_]TypeId{.s64}, .bool)), TypeResolver.resolveCompound(&table, &cnode, inner)); + + // Plain positional tuple `(s64, bool)` — owned here. + const tfields = [_]*Node{ &s64n, &booln }; + var tnode = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .tuple_type_expr = .{ .field_types = &tfields, .field_names = null } } }; + const want_tuple = table.intern(.{ .tuple = .{ .fields = &[_]TypeId{ .s64, .bool }, .names = null } }); + try std.testing.expectEqual(@as(?TypeId, want_tuple), TypeResolver.resolveCompound(&table, &tnode, inner)); + + // Pack-shaped `Closure(..p)` → null (needs caller pack state → PackResolver). + var cpack = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .closure_type_expr = .{ .param_types = &.{}, .return_type = &booln, .pack_name = "p" } } }; + try std.testing.expect(TypeResolver.resolveCompound(&table, &cpack, inner) == null); + + // Spread tuple `(..xs)` → null (a spread field needs pack expansion). + var spread_op = typeExpr("xs"); + var spread = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .spread_expr = .{ .operand = &spread_op } } }; + const sfields = [_]*Node{&spread}; + var snode = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .tuple_type_expr = .{ .field_types = &sfields, .field_names = null } } }; + try std.testing.expect(TypeResolver.resolveCompound(&table, &snode, inner) == null); + + // Names / parameterized types are not this resolver's responsibility → null. var name = typeExpr("List"); - try std.testing.expect(tr.resolveCompound(&name, inner) == null); + try std.testing.expect(TypeResolver.resolveCompound(&table, &name, inner) == null); } test "ResolveEnv default-constructs with all-null context" { diff --git a/src/ir/type_resolver.zig b/src/ir/type_resolver.zig index f43e184..2cb7166 100644 --- a/src/ir/type_resolver.zig +++ b/src/ir/type_resolver.zig @@ -7,6 +7,7 @@ const program_index_mod = @import("program_index.zig"); const Node = ast.Node; const TypeId = types.TypeId; const TypeTable = types.TypeTable; +const StringId = types.StringId; const ProgramIndex = program_index_mod.ProgramIndex; /// Explicit, caller-supplied resolution context (architecture Principle 2): @@ -66,23 +67,77 @@ pub const TypeResolver = struct { return null; } - /// Structural compound types whose meaning is fully determined by their - /// node kind and element type(s): `*T`, `[*]T`, `[]T`, `?T`, `[N]T`. - /// Element types are resolved via `inner.resolveInner(node)` so generic - /// structs / bindings / aliases in element position keep their full - /// (caller-side, stateful) resolution. Returns `null` for any other node - /// kind — names, tuples, closures/functions (pack-aware, A2.3), - /// parameterized types, pack-index, `Self` — which the caller handles. - pub fn resolveCompound(self: TypeResolver, node: *const Node, inner: anytype) ?TypeId { + /// Single owner of structural AST-type-shape construction. Builds the + /// shapes whose `TypeId` is fully determined by their node kind plus their + /// element types resolved through `inner.resolveInner`: `*T`, `[*]T`, `[]T`, + /// `?T`, `[N]T`, `(P...) -> R` functions, plain `Closure(P...) -> R`, and + /// plain positional/named tuples. Element recursion goes through `inner`, so + /// the caller's resolution mode is preserved — the compiler's stateful path + /// passes `*Lowering` (generic/pack-binding aware), `type_bridge` passes a + /// binding-free adapter. Both call THIS; there is no second compound/shape + /// algorithm (architecture A2.3b — `resolveCompound` is the single owner). + /// + /// Namespaced (no `self`): only the `TypeTable` is needed, so `type_bridge` + /// (which has no `ProgramIndex`/diagnostics) can call it too. + /// + /// Returns `null` for shapes that depend on caller pack/binding STATE and so + /// can't be built here: pack-shaped `Closure(..p)` and spread tuples + /// `(..xs)` (the stateful caller routes these to `PackResolver`), plus + /// names, parameterized types, pack-index, and `Self`. OOM yields the + /// `.unresolved` sentinel, never a fabricated type. + pub fn resolveCompound(table: *TypeTable, node: *const Node, inner: anytype) ?TypeId { return switch (node.data) { - .pointer_type_expr => |pt| self.types.ptrTo(inner.resolveInner(pt.pointee_type)), - .many_pointer_type_expr => |mp| self.types.manyPtrTo(inner.resolveInner(mp.element_type)), - .slice_type_expr => |st| self.types.sliceOf(inner.resolveInner(st.element_type)), - .optional_type_expr => |ot| self.types.optionalOf(inner.resolveInner(ot.inner_type)), + .pointer_type_expr => |pt| table.ptrTo(inner.resolveInner(pt.pointee_type)), + .many_pointer_type_expr => |mp| table.manyPtrTo(inner.resolveInner(mp.element_type)), + .slice_type_expr => |st| table.sliceOf(inner.resolveInner(st.element_type)), + .optional_type_expr => |ot| table.optionalOf(inner.resolveInner(ot.inner_type)), .array_type_expr => |at| blk: { const elem = inner.resolveInner(at.element_type); const len: u32 = if (at.length.data == .int_literal) @intCast(at.length.data.int_literal.value) else 0; - break :blk self.types.arrayOf(elem, len); + break :blk table.arrayOf(elem, len); + }, + .function_type_expr => |ft| blk: { + var param_ids = std.ArrayList(TypeId).empty; + defer param_ids.deinit(table.alloc); + for (ft.param_types) |pt| param_ids.append(table.alloc, inner.resolveInner(pt)) catch return .unresolved; + const ret_ty = if (ft.return_type) |rt| inner.resolveInner(rt) else TypeId.void; + const cc: types.TypeInfo.CallConv = switch (ft.call_conv) { + .default => .default, + .c => .c, + }; + break :blk table.functionTypeCC(param_ids.items, ret_ty, cc); + }, + .closure_type_expr => |ct| blk: { + // Pack-shaped `Closure(..p)` needs caller pack state to expand — + // defer to PackResolver (stateful) by returning null. + if (ct.pack_name != null) break :blk null; + var param_ids = std.ArrayList(TypeId).empty; + defer param_ids.deinit(table.alloc); + for (ct.param_types) |pt| param_ids.append(table.alloc, inner.resolveInner(pt)) catch return .unresolved; + const ret_ty = if (ct.return_type) |rt| inner.resolveInner(rt) else TypeId.void; + break :blk table.closureType(param_ids.items, ret_ty); + }, + .tuple_type_expr => |tt| blk: { + // A spread field `(..xs)` expands to many fields via the pack + // state — defer to PackResolver by returning null. + for (tt.field_types) |ft| if (ft.data == .spread_expr) break :blk null; + var field_ids = std.ArrayList(TypeId).empty; + defer field_ids.deinit(table.alloc); + for (tt.field_types) |ft| field_ids.append(table.alloc, inner.resolveInner(ft)) catch return .unresolved; + // Preserve field names for a named tuple `(x: T, y: U)` when the + // name and field counts agree (so `t.x` resolves). + var name_ids: ?[]const StringId = null; + if (tt.field_names) |names| { + if (names.len == field_ids.items.len) { + var ids = std.ArrayList(StringId).empty; + for (names) |n| ids.append(table.alloc, table.internString(n)) catch return .unresolved; + name_ids = ids.toOwnedSlice(table.alloc) catch null; + } + } + break :blk table.intern(.{ .tuple = .{ + .fields = table.alloc.dupe(TypeId, field_ids.items) catch return .unresolved, + .names = name_ids, + } }); }, else => null, };