refactor(ir): converge structural type-shape resolution onto resolveCompound (A2.3b)

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.
This commit is contained in:
agra
2026-06-02 15:20:31 +03:00
parent 3ed1b3a7a0
commit 9b50aacbe4
5 changed files with 199 additions and 136 deletions

View File

@@ -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 => {},
}

View File

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

View File

@@ -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| {

View File

@@ -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" {

View File

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