Merge Phase A2 (canonical type resolution) into master

Brings the architecture stream's Phase A2 onto master (A2.1–A2.3b + issue 0067):
- A2.1 (9eb85cf): ResolveEnv + TypeResolver shell (primitives + compounds).
- A2.2 (dd16bab): generic-binding + alias-aware name resolution into TypeResolver.
- A2.3 (3ed1b3a): pack projections → PackResolver; retire the TypeTable.aliases
  borrow (alias map threaded explicitly).
- A2.3b (9b50aac): converge structural type-shape resolution onto the single
  TypeResolver.resolveCompound; type_bridge reduced to a thin adapter.
- 0067 (744decc): reject non-type tuple-literal-as-type elements with a
  diagnostic instead of fabricating .s64.

Gate: zig build, zig build test, run_examples 351/0. Codex-reviewed (round 2).
This commit is contained in:
agra
2026-06-02 16:02:59 +03:00
15 changed files with 1097 additions and 586 deletions

View File

@@ -0,0 +1,13 @@
// A tuple literal used in a type position (`(s32, s32)` reinterpreted as a tuple
// type at a type-demanding site like `size_of`) must list only types. A non-type
// element — here the `1` in `(s32, 1)` — is rejected with a user-facing
// diagnostic instead of silently fabricating an `s64` field for that slot.
// Regression (issue 0067).
// Expected: a clean "tuple type element is not a type" error at the `1`; exit 1.
#import "modules/std.sx";
main :: () -> s32 {
print("bad tuple type size = {}\n", size_of((s32, 1)));
0
}

View File

@@ -0,0 +1,5 @@
error: tuple type element is not a type (found `int_literal`); a tuple used as a type must list only types, e.g. `(s32, s32)`
--> examples/1116-diagnostics-tuple-type-nontype-element-rejected.sx:11:55
|
11 | print("bad tuple type size = {}\n", size_of((s32, 1)));
| ^

View File

@@ -0,0 +1,92 @@
# 0067 — tuple literal used as a type silently accepts non-type elements
> **RESOLVED** (2026-06-02).
> **Root cause:** `type_bridge.resolveTupleLiteralAsType` treated a tuple literal
> as a tuple TYPE and, for any element that wasn't type-shaped, emitted a
> `std.debug.print` and substituted `.s64` for that field — a silent fabricated
> type (the forbidden silent-fallback pattern). The stateful caller
> (`Lowering.resolveTypeArg`, used by `size_of`) delegated `.tuple_literal`
> straight to that path, so `size_of((s32, 1))` compiled and printed `16`.
> **Fix:**
> - `type_bridge.resolveTupleLiteralAsType` now returns `.unresolved` (no `.s64`,
> no debug print) when any element is not type-shaped — it refuses to fabricate
> a tuple. (type_bridge is stateless, so this is the binding-free backstop.)
> - New stateful `Lowering.resolveTupleLiteralTypeArg` validates each element via
> `type_bridge.isTypeShapedAstNode`, emits a user-facing diagnostic at the
> offending element's span, and returns `.unresolved`. It is wired into BOTH
> `resolveTypeArg` (size_of/align_of/…) and the `resolveTypeWithBindings`
> name-fallback; type_bridge builds the tuple only after validation passes.
> **Regression test:** `examples/1116-diagnostics-tuple-type-nontype-element-rejected.sx`
> (exit 1 + diagnostic). Valid `(s32, s32)` still works
> (`examples/0115-types-compound-type-in-expression.sx`). Suite 351/0.
## Symptom
`size_of((s32, 1))` treats the tuple literal as a tuple TYPE even though `1` is
not a type. The compiler prints an internal `type_bridge` debug line, then
silently substitutes `.s64` for that slot and compiles successfully.
Observed:
```text
type_bridge: tuple literal element is not a type (tag=int_literal) — cannot use as tuple type
bad tuple type size = 16
```
Expected: a user-facing compiler diagnostic rejecting the non-type tuple element,
with no fabricated tuple type and no successful run.
## Reproduction
```sx
#import "modules/std.sx";
main :: () -> s32 {
print("bad tuple type size = {}\n", size_of((s32, 1)));
0
}
```
Run:
```sh
./zig-out/bin/sx run .sx-tmp/probe-tuple-literal-type-fallback.sx
```
The repro is standalone; the inline source above is sufficient to recreate the
scratch file under `.sx-tmp/`.
## Investigation prompt
Fix issue 0067: tuple literals reinterpreted as tuple types must reject non-type
elements instead of silently fabricating `.s64` fields.
Suspected area:
- `src/ir/type_bridge.zig`, `resolveTupleLiteralAsType`
- The current non-type branch does `std.debug.print(...)` and
`field_ids.append(alloc, .s64)`, which violates the compiler fallback rules.
- Related callers: `type_bridge.resolveAstType` for `.tuple_literal`, and
`Lowering.resolveTypeWithBindings` fallback paths that reach `type_bridge`.
Likely fix:
- Replace the `.s64` substitution with a real diagnostic path and an
unmistakable failure result (`.unresolved`, or a nullable/result return that
forces callers to handle the failure).
- Make the diagnostic user-facing via the lowering diagnostics plumbing, not
`std.debug.print`.
- Preserve the valid behavior pinned by `examples/0115-types-compound-type-in-expression.sx`,
where `(s32, s32)` in a type-demanding site resolves as a tuple type.
Verification:
- Add a focused diagnostics example in the `11xx` block for
`size_of((s32, 1))` expecting exit 1 and a clear diagnostic.
- Run:
```sh
zig build
zig build test
bash tests/run_examples.sh
```
Expected result: the new invalid tuple-type repro fails with a diagnostic, the
valid `0115` tuple-type example still passes, and the full suite remains green.

View File

@@ -5,6 +5,8 @@ pub const print = @import("print.zig");
pub const interp = @import("interp.zig");
pub const lower = @import("lower.zig");
pub const program_index = @import("program_index.zig");
pub const type_resolver = @import("type_resolver.zig");
pub const packs = @import("packs.zig");
pub const TypeId = types.TypeId;
pub const TypeInfo = types.TypeInfo;
@@ -32,6 +34,9 @@ pub const Interpreter = interp.Interpreter;
pub const Value = interp.Value;
pub const Lowering = lower.Lowering;
pub const ProgramIndex = program_index.ProgramIndex;
pub const TypeResolver = type_resolver.TypeResolver;
pub const ResolveEnv = type_resolver.ResolveEnv;
pub const PackResolver = packs.PackResolver;
pub const compiler_hooks = @import("compiler_hooks.zig");
pub const emit_llvm = @import("emit_llvm.zig");
@@ -51,6 +56,8 @@ pub const print_tests = @import("print.test.zig");
pub const interp_tests = @import("interp.test.zig");
pub const lower_tests = @import("lower.test.zig");
pub const program_index_tests = @import("program_index.test.zig");
pub const type_resolver_tests = @import("type_resolver.test.zig");
pub const packs_tests = @import("packs.test.zig");
pub const type_bridge_tests = @import("type_bridge.test.zig");
pub const emit_llvm_tests = @import("emit_llvm.test.zig");
pub const jni_descriptor_tests = @import("jni_descriptor.test.zig");

View File

@@ -19,6 +19,9 @@ const TemplateParam = program_index_mod.TemplateParam;
const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo;
const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo;
const ModuleConstInfo = program_index_mod.ModuleConstInfo;
const TypeResolver = @import("type_resolver.zig").TypeResolver;
const ResolveEnv = @import("type_resolver.zig").ResolveEnv;
const PackResolver = @import("packs.zig").PackResolver;
const TypeId = types.TypeId;
const StringId = types.StringId;
@@ -286,12 +289,6 @@ pub const Lowering = struct {
/// Pass 1: Scan all declarations (register ASTs, types, extern stubs).
/// Pass 2: Lower only `main` (everything else is lowered lazily on demand).
pub fn lowerRoot(self: *Lowering, root: *const Node) void {
// Loan our alias map to the TypeTable. Done here (not in
// init) because `init` returns by value and `&self.program_index.type_alias_map`
// wouldn't survive the return. `lowerRoot` runs on the
// caller's stable Lowering, so the borrow stays valid for
// every subsequent `resolveAstType` / `resolveTypeName` call.
self.module.types.aliases = &self.program_index.type_alias_map;
const decls = switch (root.data) {
.root => |r| r.decls,
else => return,
@@ -1340,9 +1337,9 @@ pub const Lowering = struct {
} else if (cd.value.data == .struct_decl) {
self.registerStructDecl(&cd.value.data.struct_decl);
} else if (cd.value.data == .enum_decl) {
_ = type_bridge.resolveAstType(cd.value, &self.module.types);
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map);
} else if (cd.value.data == .union_decl) {
_ = type_bridge.resolveAstType(cd.value, &self.module.types);
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map);
} else if (cd.value.data == .comptime_expr) {
self.lowerComptimeGlobal(cd.name, cd.value.data.comptime_expr.expr, cd.type_annotation);
}
@@ -1354,10 +1351,10 @@ pub const Lowering = struct {
self.registerStructDecl(&sd);
},
.enum_decl => {
_ = type_bridge.resolveAstType(decl, &self.module.types);
_ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map);
},
.union_decl => {
_ = type_bridge.resolveAstType(decl, &self.module.types);
_ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map);
},
.error_set_decl => {
self.registerErrorSetDecl(decl);
@@ -1455,10 +1452,10 @@ pub const Lowering = struct {
self.registerStructDecl(&cd.value.data.struct_decl);
} else if (cd.value.data == .enum_decl) {
// Register enum/tagged-union types in the type table
_ = type_bridge.resolveAstType(cd.value, &self.module.types);
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map);
} else if (cd.value.data == .union_decl) {
// Register plain union types in the type table
_ = type_bridge.resolveAstType(cd.value, &self.module.types);
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map);
} else if (cd.value.data == .type_expr or
cd.value.data == .pointer_type_expr or
cd.value.data == .many_pointer_type_expr or
@@ -1468,7 +1465,7 @@ pub const Lowering = struct {
cd.value.data == .function_type_expr)
{
// Type alias: MyFloat :: f64; Ptr :: *u8; Cb :: (s32) -> s32;
const target_ty = type_bridge.resolveAstType(cd.value, &self.module.types);
const target_ty = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map);
self.program_index.type_alias_map.put(cd.name, target_ty) catch {};
} else if (cd.value.data == .identifier) {
// Identifier-RHS alias: MyAlias :: MyInt; WideAlias :: Wide;
@@ -1551,7 +1548,7 @@ pub const Lowering = struct {
// resolve via type_bridge and register the result
// under the alias name so `Vec4` in expression
// position can `const_type(<vector tid>)`.
const result_ty = type_bridge.resolveAstType(cd.value, &self.module.types);
const result_ty = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map);
if (result_ty != .void and result_ty != .unresolved) {
self.program_index.type_alias_map.put(cd.name, result_ty) catch {};
}
@@ -1589,11 +1586,11 @@ pub const Lowering = struct {
},
.enum_decl => {
// Register enum/tagged-union types in the type table
_ = type_bridge.resolveAstType(decl, &self.module.types);
_ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map);
},
.union_decl => {
// Register plain union types in the type table
_ = type_bridge.resolveAstType(decl, &self.module.types);
_ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map);
},
.error_set_decl => {
self.registerErrorSetDecl(decl);
@@ -2420,7 +2417,7 @@ pub const Lowering = struct {
// Block-local type declarations
.struct_decl => |sd| self.registerStructDecl(&sd),
.enum_decl, .union_decl => {
_ = type_bridge.resolveAstType(node, &self.module.types);
_ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map);
},
.error_set_decl => self.registerErrorSetDecl(node),
.ufcs_alias => |ua| {
@@ -2579,7 +2576,7 @@ pub const Lowering = struct {
return;
}
if (cd.value.data == .enum_decl or cd.value.data == .union_decl) {
_ = type_bridge.resolveAstType(cd.value, &self.module.types);
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map);
return;
}
@@ -3504,7 +3501,7 @@ pub const Lowering = struct {
// `t : Type = f64;` store a real TypeId; lets
// `t == f64` icmp at runtime against the same TypeId.
if (self.isKnownTypeName(te.name)) {
const ty = type_bridge.resolveAstType(node, &self.module.types);
const ty = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map);
break :blk self.builder.constType(ty);
}
break :blk self.emitError(te.name, node.span);
@@ -6039,7 +6036,7 @@ pub const Lowering = struct {
const name_id = self.module.types.internString(id.name);
return self.module.types.findByName(name_id) orelse .unresolved;
},
.type_expr => return type_bridge.resolveAstType(te, &self.module.types),
.type_expr => return type_bridge.resolveAstType(te, &self.module.types, &self.program_index.type_alias_map),
.field_access => |fa| {
// Module.Type — try to resolve the field as a type name
const name_id = self.module.types.internString(fa.field);
@@ -7797,7 +7794,7 @@ pub const Lowering = struct {
// Check for #compiler free functions
if (self.program_index.fn_ast_map.get(func_name)) |fd_check| {
if (fd_check.body.data == .compiler_expr) {
const ret_ty = if (fd_check.return_type) |rt| type_bridge.resolveAstType(rt, &self.module.types) else TypeId.void;
const ret_ty = if (fd_check.return_type) |rt| type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map) else TypeId.void;
return self.builder.compilerCall(func_name, args.items, ret_ty);
}
}
@@ -8165,7 +8162,7 @@ pub const Lowering = struct {
if (self.program_index.fn_ast_map.get(qualified)) |method_fd| {
if (method_fd.body.data == .compiler_expr) {
const ret_ty = if (method_fd.return_type) |rt|
type_bridge.resolveAstType(rt, &self.module.types)
type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map)
else
.void;
return self.builder.compilerCall(qualified, method_args.items, ret_ty);
@@ -8603,7 +8600,7 @@ pub const Lowering = struct {
const ret_ty = blk: {
if (lam.return_type) |rt| {
break :blk type_bridge.resolveAstType(rt, &self.module.types);
break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map);
}
// Use target closure return type if available — but only when it's
// a resolved type. An `.unresolved` ret comes from an unbound
@@ -9160,7 +9157,7 @@ pub const Lowering = struct {
}
fn resolveReturnType2(self: *Lowering, rt: ?*const Node) TypeId {
if (rt) |r| return type_bridge.resolveAstType(r, &self.module.types);
if (rt) |r| return type_bridge.resolveAstType(r, &self.module.types, &self.program_index.type_alias_map);
return .void;
}
@@ -10298,8 +10295,8 @@ pub const Lowering = struct {
const ret_ty: TypeId = blk: {
if (fd.return_type) |rt| {
if (rt.data == .type_expr) {
if (type_bridge.resolveAstType(rt, &self.module.types) != .unresolved) {
break :blk type_bridge.resolveAstType(rt, &self.module.types);
if (type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map) != .unresolved) {
break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map);
}
}
}
@@ -11589,6 +11586,25 @@ pub const Lowering = struct {
}
}
/// Resolve a tuple LITERAL used in a type position (`(s32, s32)` reinterpreted
/// as a tuple type at a type-demanding site such as `size_of`). Every element
/// must itself denote a type; a non-type element — e.g. the `1` in
/// `(s32, 1)` — is a user error. Emit a diagnostic pointing at the offending
/// element and return `.unresolved`; never fabricate a tuple with a bogus
/// field (issue 0067). type_bridge.resolveAstType builds the tuple only after
/// this validation passes.
fn resolveTupleLiteralTypeArg(self: *Lowering, node: *const Node) TypeId {
for (node.data.tuple_literal.elements) |el| {
if (!type_bridge.isTypeShapedAstNode(el.value, &self.module.types)) {
if (self.diagnostics) |diags| {
diags.addFmt(.err, el.value.span, "tuple type element is not a type (found `{s}`); a tuple used as a type must list only types, e.g. `(s32, s32)`", .{@tagName(el.value.data)});
}
return .unresolved;
}
}
return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map);
}
fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId {
// Pack-index access in a type-arg slot (e.g. `type_name($args[0])`
// or `type_eq($args[i], s64)`). Same shape as the
@@ -11647,7 +11663,7 @@ pub const Lowering = struct {
},
.type_expr => |te| {
if (self.program_index.type_alias_map.get(te.name)) |alias_ty| return alias_ty;
return type_bridge.resolveAstType(node, &self.module.types);
return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map);
},
.call => |cl| {
// `type_of(x)` resolves to `inferExprType(x)` at lower
@@ -11665,14 +11681,14 @@ pub const Lowering = struct {
// Handle type constructor calls: size_of(Sx(f32)), size_of(Complex(u32))
return self.resolveTypeCallWithBindings(&cl);
},
.tuple_literal => return self.resolveTupleLiteralTypeArg(node),
.pointer_type_expr,
.many_pointer_type_expr,
.array_type_expr,
.slice_type_expr,
.optional_type_expr,
.function_type_expr,
.tuple_literal,
=> return type_bridge.resolveAstType(node, &self.module.types),
=> return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map),
else => return .unresolved,
}
}
@@ -11872,7 +11888,7 @@ pub const Lowering = struct {
};
}
fn mangleTypeName(self: *Lowering, ty: TypeId) []const u8 {
pub fn mangleTypeName(self: *Lowering, ty: TypeId) []const u8 {
// Builtin types
if (ty == .s8) return "s8";
if (ty == .s16) return "s16";
@@ -12730,8 +12746,36 @@ pub const Lowering = struct {
return self.resolveTypeWithBindings(type_ann);
}
/// Construct a `TypeResolver` view over the current lowering state (borrows
/// only; cheap by-value, reflects current `diagnostics` / `program_index`).
fn typeResolver(self: *Lowering) TypeResolver {
return .{
.alloc = self.alloc,
.types = &self.module.types,
.diagnostics = self.diagnostics,
.index = &self.program_index,
};
}
/// Snapshot the active resolution context (Principle 2) for `TypeResolver`.
/// A2.2 wires the type bindings + literal target; the pack/comptime fields
/// are populated as A2.3 moves the cases that consume them.
fn resolveEnv(self: *Lowering) ResolveEnv {
return .{
.type_bindings = if (self.type_bindings) |*tb| tb else null,
.target_type = self.target_type,
};
}
/// Inner-type recursion hook for `TypeResolver.resolveCompound`: resolves a
/// child type node through the full stateful resolver, so generic structs /
/// bindings / aliases in element position keep their resolution.
pub fn resolveInner(self: *Lowering, node: *const Node) TypeId {
return self.resolveTypeWithBindings(node);
}
/// Resolve a type node, checking type_bindings first for generic type params.
fn resolveTypeWithBindings(self: *Lowering, node: *const Node) TypeId {
pub fn resolveTypeWithBindings(self: *Lowering, node: *const Node) TypeId {
// Pack-index in a type position: `$<pack>[<lit>]` resolves to the
// i-th element type of the active pack binding (step 3 of the
// variadic heterogeneous type packs feature). Unblocks parametric
@@ -12774,59 +12818,19 @@ pub const Lowering = struct {
}
}
}
if (self.type_bindings) |tb| {
switch (node.data) {
.type_expr => |te| {
// Check bindings for any type_expr name — not just those
// marked is_generic. The return type `T` in `-> T` may
// not have the `$` prefix, so is_generic is false, but
// it still refers to the type param.
if (tb.get(te.name)) |ty| return ty;
},
.identifier => |id| {
if (tb.get(id.name)) |ty| return ty;
},
// Compound types: resolve inner types with bindings
.slice_type_expr => |st| {
const elem = self.resolveTypeWithBindings(st.element_type);
return self.module.types.sliceOf(elem);
},
.pointer_type_expr => |pt| {
const pointee = self.resolveTypeWithBindings(pt.pointee_type);
return self.module.types.ptrTo(pointee);
},
.many_pointer_type_expr => |mp| {
const elem = self.resolveTypeWithBindings(mp.element_type);
return self.module.types.manyPtrTo(elem);
},
.optional_type_expr => |ot| {
const child = self.resolveTypeWithBindings(ot.inner_type);
return self.module.types.optionalOf(child);
},
.array_type_expr => |at| {
const elem = self.resolveTypeWithBindings(at.element_type);
const len: u32 = blk: {
if (at.length.data == .int_literal) break :blk @intCast(at.length.data.int_literal.value);
break :blk 0;
};
return self.module.types.arrayOf(elem, len);
},
.parameterized_type_expr => |pt| {
return self.resolveParameterizedWithBindings(&pt);
},
.call => |cl| {
// Handle List(T), Vector(N, T) etc. as type constructor calls
return self.resolveTypeCallWithBindings(&cl);
},
.closure_type_expr => |ct| {
return self.resolveClosureTypeWithBindings(&ct);
},
.function_type_expr => |ft| {
return self.resolveFunctionTypeWithBindings(&ft);
},
else => {},
}
}
// 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
// here were redundant with the unconditional handling just below (both
// read the active bindings through the same resolvers), so they're gone.
if (TypeResolver.resolveBinding(node, self.resolveEnv())) |t| return t;
// Even without active type_bindings, handle parameterized types with struct templates
if (node.data == .parameterized_type_expr) {
return self.resolveParameterizedWithBindings(&node.data.parameterized_type_expr);
@@ -12834,68 +12838,23 @@ pub const Lowering = struct {
if (node.data == .call) {
return self.resolveTypeCallWithBindings(&node.data.call);
}
// Handle compound types that may contain generic structs (e.g., *List(ViewChild))
// These need the lowerer's resolveType to properly instantiate generics.
// 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) {
.pointer_type_expr => |pt| {
const pointee = self.resolveTypeWithBindings(pt.pointee_type);
return self.module.types.ptrTo(pointee);
},
.slice_type_expr => |st| {
const elem = self.resolveTypeWithBindings(st.element_type);
return self.module.types.sliceOf(elem);
},
.many_pointer_type_expr => |mp| {
const elem = self.resolveTypeWithBindings(mp.element_type);
return self.module.types.manyPtrTo(elem);
},
.optional_type_expr => |ot| {
const child = self.resolveTypeWithBindings(ot.inner_type);
return self.module.types.optionalOf(child);
},
.array_type_expr => |at| {
const elem = self.resolveTypeWithBindings(at.element_type);
const len: u32 = if (at.length.data == .int_literal) @intCast(at.length.data.int_literal.value) else 0;
return self.module.types.arrayOf(elem, len);
},
.closure_type_expr => |ct| {
return self.resolveClosureTypeWithBindings(&ct);
},
.function_type_expr => |ft| {
return self.resolveFunctionTypeWithBindings(&ft);
return self.packResolver().resolveClosureTypeWithBindings(&ct);
},
.tuple_type_expr => |tt| {
return self.resolveTupleTypeWithBindings(&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.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 => {},
}
@@ -12906,202 +12865,27 @@ pub const Lowering = struct {
if (node.data == .type_expr and node.data.type_expr.is_generic) {
return .unresolved;
}
// Alias resolution (`ShaderHandle :: u32`, `Vec4 ::
// Vector(4,f32)`) is now handled inside `resolveTypeName`
// via the `TypeTable.aliases` borrow loaned at lowerRoot.
return type_bridge.resolveAstType(node, &self.module.types);
// Bare type names resolve through TypeResolver, which reads the
// canonical alias table directly (`ProgramIndex.type_alias_map`). Other
// node kinds (inline type decls, error types) still route through
// type_bridge, which now takes the alias map as an explicit argument
// (the `TypeTable.aliases` borrow is gone, A2.3).
switch (node.data) {
.type_expr => |te| return self.typeResolver().resolveName(te.name),
.identifier => |id| return self.typeResolver().resolveName(id.name),
// A non-spread tuple literal in a type position is a tuple-type
// literal (`(s32, s32)`); validate its elements are types and reject
// non-type elements loudly (issue 0067).
.tuple_literal => return self.resolveTupleLiteralTypeArg(node),
else => return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map),
}
}
/// Resolve a `Closure(...)` type expression with the active type/pack
/// bindings applied. Pack-shaped closure exprs (`Closure(Prefix..., ..$pack)`)
/// substitute `pack` from `self.pack_bindings`, producing a concrete
/// closure type — used when monomorphising a pack-variadic impl body
/// against a concrete source signature.
fn resolveClosureTypeWithBindings(self: *Lowering, ct: *const ast.ClosureTypeExpr) TypeId {
var param_ids = std.ArrayList(TypeId).empty;
defer param_ids.deinit(self.alloc);
for (ct.param_types) |pt| {
param_ids.append(self.alloc, self.resolveTypeWithBindings(pt)) catch return .void;
}
if (ct.pack_name) |pn| {
// Protocol pack (`Closure(..sources.T)` / `Closure(..sources)`):
// expand the bound pack's per-element type-args.
if (self.packTypeArgs(pn, ct.pack_projection)) |elems| {
defer self.alloc.free(elems);
for (elems) |t| param_ids.append(self.alloc, t) catch return .void;
const ret_ty = if (ct.return_type) |rt| self.resolveTypeWithBindings(rt) else .void;
return self.module.types.closureType(param_ids.items, ret_ty);
}
if (self.pack_bindings) |pb| {
if (pb.get(pn)) |pack_tys| {
for (pack_tys) |t| param_ids.append(self.alloc, t) catch return .void;
// Fully bound — emit a concrete closure type, no pack_start.
const ret_ty = if (ct.return_type) |rt| self.resolveTypeWithBindings(rt) else .void;
return self.module.types.closureType(param_ids.items, ret_ty);
}
}
// Pack name in scope but no binding — preserve the pack-shape
// so downstream code can still see it's variadic. (Hit during
// impl-block parsing before any concrete monomorphisation.)
const ret_ty = if (ct.return_type) |rt| self.resolveTypeWithBindings(rt) else .void;
return self.module.types.closureTypePack(param_ids.items, ret_ty, @intCast(param_ids.items.len));
}
const ret_ty = if (ct.return_type) |rt| self.resolveTypeWithBindings(rt) else .void;
return self.module.types.closureType(param_ids.items, ret_ty);
}
/// Resolve a tuple type expression with active pack bindings: a spread field
/// `(..xs)` / `(..xs.T)` expands to the pack's per-element types via
/// `packTypeElems`. Non-spread fields resolve normally.
fn resolveTupleTypeWithBindings(self: *Lowering, tt: *const ast.TupleTypeExpr) TypeId {
var field_ids = std.ArrayList(TypeId).empty;
defer field_ids.deinit(self.alloc);
var had_spread = false;
for (tt.field_types) |ft| {
if (ft.data == .spread_expr) {
if (self.packTypeElems(ft.data.spread_expr.operand)) |elems| {
defer self.alloc.free(elems);
for (elems) |e| field_ids.append(self.alloc, e) catch return .void;
had_spread = true;
continue;
}
}
field_ids.append(self.alloc, self.resolveTypeWithBindings(ft)) catch return .void;
}
// Preserve field names for a named tuple `(x: T, y: U)` so `t.x` resolves
// (matches type_bridge.resolveTupleType). A spread expands to unnamed
// pack elements, so names only apply when there was no spread.
var name_ids: ?[]const types.StringId = null;
if (!had_spread) {
if (tt.field_names) |names| {
if (names.len == field_ids.items.len) {
var ids = std.ArrayList(types.StringId).empty;
for (names) |n| ids.append(self.alloc, self.module.types.internString(n)) catch return .void;
name_ids = ids.toOwnedSlice(self.alloc) catch null;
}
}
}
return self.module.types.intern(.{ .tuple = .{
.fields = self.alloc.dupe(TypeId, field_ids.items) catch return .void,
.names = name_ids,
} });
}
/// TYPE-position pack expansion: given a spread operand, return the
/// per-element types. `..xs` → the pack's element types (`pack_arg_types`).
/// `..xs.T` → each element's protocol type-arg `T` (from its
/// `impl P(args) for elem` in `param_impl_map`). Null when not a pack spread.
/// Caller owns the returned slice.
fn packTypeElems(self: *Lowering, operand: *const Node) ?[]TypeId {
const pat = self.pack_arg_types orelse return null;
// `..F(Ts)` — apply a parameterized type `F` to each pack element:
// `(..VL(Ts))` → `(VL(T0), VL(T1), …)`. Per element, temporarily bind
// the pack name to that single element type and resolve `F(elem)`.
if (operand.data == .parameterized_type_expr) {
const pt = operand.data.parameterized_type_expr;
var pack_name_p: []const u8 = "";
for (pt.args) |a| {
const nm = switch (a.data) {
.identifier => |id| id.name,
.type_expr => |te| te.name,
else => continue,
};
if (pat.contains(nm)) {
pack_name_p = nm;
break;
}
}
if (pack_name_p.len == 0) return null;
const elems = pat.get(pack_name_p) orelse return null;
if (self.type_bindings == null) return null;
var out = std.ArrayList(TypeId).empty;
for (elems) |ti| {
const had = self.type_bindings.?.get(pack_name_p);
self.type_bindings.?.put(pack_name_p, ti) catch {};
out.append(self.alloc, self.resolveTypeWithBindings(operand)) catch return null;
if (had) |h| self.type_bindings.?.put(pack_name_p, h) catch {} else _ = self.type_bindings.?.remove(pack_name_p);
}
return out.toOwnedSlice(self.alloc) catch null;
}
// In type position `xs` / `xs.T` parse to a (possibly dotted) type_expr
// name; `field_access` covers any value-shaped form.
var pack_name: []const u8 = "";
var projection: ?[]const u8 = null;
switch (operand.data) {
.type_expr, .identifier => {
const full = if (operand.data == .type_expr) operand.data.type_expr.name else operand.data.identifier.name;
if (std.mem.indexOfScalar(u8, full, '.')) |dot| {
pack_name = full[0..dot];
projection = full[dot + 1 ..];
} else {
pack_name = full;
}
},
.field_access => |fa| {
pack_name = switch (fa.object.data) {
.identifier => |id| id.name,
.type_expr => |te| te.name,
else => return null,
};
projection = fa.field;
},
else => return null,
}
return self.packTypeArgs(pack_name, projection);
}
/// Per-element types for a bound protocol pack: `pack_name` alone → the
/// element types; with `projection` (`xs.T`) → each element's protocol
/// type-arg. Null when `pack_name` isn't a bound pack. Caller owns the slice.
fn packTypeArgs(self: *Lowering, pack_name: []const u8, projection: ?[]const u8) ?[]TypeId {
const pat = self.pack_arg_types orelse return null;
const elems = pat.get(pack_name) orelse return null;
if (projection == null) return self.alloc.dupe(TypeId, elems) catch null;
const proto = if (self.pack_constraint) |pc| (pc.get(pack_name) orelse return null) else return null;
const arg_idx = self.lookupProtocolArg(proto, projection.?) orelse return null;
var out = std.ArrayList(TypeId).empty;
for (elems) |elem| {
out.append(self.alloc, self.elementProtocolTypeArg(proto, elem, arg_idx) orelse .void) catch return null;
}
return out.toOwnedSlice(self.alloc) catch null;
}
/// For a concrete `elem` conforming to parameterised `proto`, return the
/// `arg_idx`-th protocol type-arg from its `impl proto(args) for elem`
/// (scans `param_impl_map` for `proto\x00…\x00mangle(elem)`).
fn elementProtocolTypeArg(self: *Lowering, proto: []const u8, elem: TypeId, arg_idx: u32) ?TypeId {
const prefix = std.fmt.allocPrint(self.alloc, "{s}\x00", .{proto}) catch return null;
const suffix = std.fmt.allocPrint(self.alloc, "\x00{s}", .{self.mangleTypeName(elem)}) catch return null;
var it = self.param_impl_map.iterator();
while (it.next()) |entry| {
const k = entry.key_ptr.*;
if (std.mem.startsWith(u8, k, prefix) and std.mem.endsWith(u8, k, suffix)) {
for (entry.value_ptr.items) |impl| {
if (arg_idx < impl.target_args.len) return impl.target_args[arg_idx];
}
}
}
return null;
}
/// 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.
fn resolveFunctionTypeWithBindings(self: *Lowering, ft: *const ast.FunctionTypeExpr) TypeId {
var param_ids = std.ArrayList(TypeId).empty;
defer param_ids.deinit(self.alloc);
for (ft.param_types) |pt| {
param_ids.append(self.alloc, self.resolveTypeWithBindings(pt)) catch return .void;
}
const ret_ty = if (ft.return_type) |rt| self.resolveTypeWithBindings(rt) else .void;
const cc: types.TypeInfo.CallConv = switch (ft.call_conv) {
.default => .default,
.c => .c,
};
return self.module.types.functionTypeCC(param_ids.items, ret_ty, cc);
/// Bind a `PackResolver` to this Lowering for pack-aware TYPE-position
/// resolution (`Closure(..p)` / `(Params...) -> R` / `(..xs)` tuples and
/// their `..xs.T` projections). A2.3 moved that logic into `packs.zig`.
fn packResolver(self: *Lowering) PackResolver {
return .{ .l = self };
}
/// Resolve a .call node that represents a type constructor (e.g., List(T), Vector(N, T)).
@@ -13223,7 +13007,7 @@ pub const Lowering = struct {
// A spread arg `..sources.T` expands to the source pack's
// per-element (projected) types; a plain arg is one type.
if (a.data == .spread_expr) {
if (self.packTypeElems(a.data.spread_expr.operand)) |elems| {
if (self.packResolver().packTypeElems(a.data.spread_expr.operand)) |elems| {
defer self.alloc.free(elems);
for (elems) |ty| {
pack_tys.append(self.alloc, ty) catch {};
@@ -13524,7 +13308,7 @@ pub const Lowering = struct {
}
return;
}
_ = type_bridge.resolveAstType(node, &self.module.types);
_ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map);
}
fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl) void {
@@ -13670,7 +13454,7 @@ pub const Lowering = struct {
if (const_node.data == .const_decl) {
const cd = const_node.data.const_decl;
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, cd.name }) catch continue;
const ty: ?TypeId = if (cd.type_annotation) |ta| type_bridge.resolveAstType(ta, table) else null;
const ty: ?TypeId = if (cd.type_annotation) |ta| type_bridge.resolveAstType(ta, table, &self.program_index.type_alias_map) else null;
self.struct_const_map.put(qualified, .{ .value = cd.value, .ty = ty }) catch {};
}
}
@@ -13795,13 +13579,13 @@ pub const Lowering = struct {
var ptypes = std.ArrayList(TypeId).empty;
for (method.params) |p| {
// Self → *void for protocol context; everything else
// goes through `resolveAstType`, which now consults
// the alias map via `TypeTable.aliases`.
// goes through `resolveAstType`, threaded with the canonical
// alias map (`ProgramIndex.type_alias_map`).
const pty = blk: {
if (p.data == .type_expr and std.mem.eql(u8, p.data.type_expr.name, "Self")) {
break :blk void_ptr_ty;
}
break :blk type_bridge.resolveAstType(p, table);
break :blk type_bridge.resolveAstType(p, table, &self.program_index.type_alias_map);
};
ptypes.append(self.alloc, pty) catch unreachable;
}
@@ -13811,7 +13595,7 @@ pub const Lowering = struct {
ret_is_self = true;
break :blk void_ptr_ty;
}
break :blk type_bridge.resolveAstType(rt, table);
break :blk type_bridge.resolveAstType(rt, table, &self.program_index.type_alias_map);
} else .void;
method_infos.append(self.alloc, .{
.name = method.name,
@@ -14359,7 +14143,7 @@ pub const Lowering = struct {
// Resolve the protocol's type-arg list to concrete TypeIds.
var arg_tys = std.ArrayList(TypeId).empty;
for (ib.protocol_type_args) |arg_node| {
const t = type_bridge.resolveAstType(arg_node, table);
const t = type_bridge.resolveAstType(arg_node, table, &self.program_index.type_alias_map);
arg_tys.append(self.alloc, t) catch return;
}
@@ -14367,9 +14151,9 @@ pub const Lowering = struct {
// parameterised impls (back-compat `target_type` string is kept for
// simple cases but the canonical form is the TypeExpr).
const src_ty: TypeId = if (ib.target_type_expr) |te|
type_bridge.resolveAstType(te, table)
type_bridge.resolveAstType(te, table, &self.program_index.type_alias_map)
else if (ib.target_type.len > 0)
type_bridge.resolveAstType(&.{ .span = decl.span, .data = .{ .type_expr = .{ .name = ib.target_type } } }, table)
type_bridge.resolveAstType(&.{ .span = decl.span, .data = .{ .type_expr = .{ .name = ib.target_type } } }, table, &self.program_index.type_alias_map)
else
return;
@@ -15152,7 +14936,7 @@ pub const Lowering = struct {
// Generic #compiler method dispatch — return type from declaration
if (self.program_index.fn_ast_map.get(qualified)) |method_fd| {
if (method_fd.body.data == .compiler_expr) {
if (method_fd.return_type) |rt| return type_bridge.resolveAstType(rt, &self.module.types);
if (method_fd.return_type) |rt| return type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map);
return .void;
}
}

88
src/ir/packs.test.zig Normal file
View File

@@ -0,0 +1,88 @@
// Tests for packs.zig (PackResolver) — pack-aware TYPE-position resolution.
const std = @import("std");
const ast = @import("../ast.zig");
const errors = @import("../errors.zig");
const ir_mod = @import("ir.zig");
const TypeId = ir_mod.TypeId;
const Lowering = ir_mod.Lowering;
const PackResolver = ir_mod.PackResolver;
test "PackResolver.packTypeArgs: bound pack → element types; unbound → null" {
const alloc = std.testing.allocator;
var module = ir_mod.Module.init(alloc);
defer module.deinit();
var lowering = Lowering.init(&module);
var pat = std.StringHashMap([]const TypeId).init(alloc);
defer pat.deinit();
const elems = [_]TypeId{ .s32, .s64 };
try pat.put("xs", &elems);
lowering.pack_arg_types = pat;
const pr = PackResolver{ .l = &lowering };
// Bound pack, no projection → a fresh copy of its element types.
const got = pr.packTypeArgs("xs", null) orelse return error.TestUnexpectedResult;
defer alloc.free(got);
try std.testing.expectEqualSlices(TypeId, &elems, got);
// Unbound pack name → null (caller continues with other resolution).
try std.testing.expect(pr.packTypeArgs("ys", null) == null);
// A projection (`xs.T`) with no constraint map → null: there is no
// protocol to project the type-arg through.
try std.testing.expect(pr.packTypeArgs("xs", "T") == null);
}
test "PackResolver.packTypeArgs: no active pack_arg_types → null" {
const alloc = std.testing.allocator;
var module = ir_mod.Module.init(alloc);
defer module.deinit();
var lowering = Lowering.init(&module);
// pack_arg_types stays null (no active pack binding).
const pr = PackResolver{ .l = &lowering };
try std.testing.expect(pr.packTypeArgs("xs", null) == null);
}
test "PackResolver.packTypeArgs: missing projection → diagnostic + .unresolved (never silent .void)" {
// Arena-backed: the projection path allocates mangle/key buffers the
// arena-style compiler never frees individually.
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var module = ir_mod.Module.init(alloc);
var lowering = Lowering.init(&module);
// Protocol `P(T: Type)` so `lookupProtocolArg("P", "T")` resolves to arg 0 —
// but with NO `impl P(...) for <elem>` registered, the per-element
// projection finds no type for the slot.
var constraint = ast.Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "Type" } } };
const tparams = [_]ast.StructTypeParam{.{ .name = "T", .constraint = &constraint }};
const pd = ast.ProtocolDecl{ .name = "P", .methods = &.{}, .type_params = &tparams };
try lowering.program_index.protocol_ast_map.put("P", &pd);
var pat = std.StringHashMap([]const TypeId).init(alloc);
const elems = [_]TypeId{.s64};
try pat.put("xs", &elems);
lowering.pack_arg_types = pat;
var pcon = std.StringHashMap([]const u8).init(alloc);
try pcon.put("xs", "P");
lowering.pack_constraint = pcon;
var diags = errors.DiagnosticList.init(alloc, "", "<test>");
lowering.diagnostics = &diags;
const pr = PackResolver{ .l = &lowering };
const got = pr.packTypeArgs("xs", "T") orelse return error.TestUnexpectedResult;
// The unfilled slot is the dedicated failure sentinel — never a real
// `.void`, which would read as a legitimate type and silently corrupt.
try std.testing.expectEqual(@as(usize, 1), got.len);
try std.testing.expectEqual(TypeId.unresolved, got[0]);
try std.testing.expect(TypeId.unresolved != TypeId.void);
// And the failure was surfaced loudly, not swallowed.
try std.testing.expect(diags.hasErrors());
}

241
src/ir/packs.zig Normal file
View File

@@ -0,0 +1,241 @@
const std = @import("std");
const ast = @import("../ast.zig");
const types = @import("types.zig");
const lower = @import("lower.zig");
const Node = ast.Node;
const TypeId = types.TypeId;
const Lowering = lower.Lowering;
/// Canonical owner of pack-aware TYPE-position resolution (architecture phase
/// A2.3). Resolves the shapes whose meaning depends on active pack state —
/// pack-variadic `Closure(..p)` / `(Params...) -> R` / `(..xs)` tuples and the
/// pack projections (`..xs.T`) that back them — in one place instead of inline
/// in `Lowering`.
///
/// A `*Lowering` facade (Principle 5): pack projection reads the live pack
/// state (`pack_arg_types` / `pack_constraint` / `pack_bindings` /
/// `type_bindings` / `param_impl_map`) and recurses through the full stateful
/// type resolver, so it borrows `Lowering` rather than re-threading every
/// field. The dependency shrinks as later phases lift pack state into an
/// explicit context object.
pub const PackResolver = struct {
l: *Lowering,
/// Resolve a `Closure(...)` type expression with the active type/pack
/// bindings applied. Pack-shaped closure exprs (`Closure(Prefix..., ..$pack)`)
/// substitute `pack` from `pack_bindings`, producing a concrete closure
/// type — used when monomorphising a pack-variadic impl body against a
/// concrete source signature.
pub fn resolveClosureTypeWithBindings(self: PackResolver, ct: *const ast.ClosureTypeExpr) TypeId {
var param_ids = std.ArrayList(TypeId).empty;
defer param_ids.deinit(self.l.alloc);
for (ct.param_types) |pt| {
param_ids.append(self.l.alloc, self.l.resolveTypeWithBindings(pt)) catch return .unresolved;
}
if (ct.pack_name) |pn| {
// Protocol pack (`Closure(..sources.T)` / `Closure(..sources)`):
// expand the bound pack's per-element type-args.
if (self.packTypeArgs(pn, ct.pack_projection)) |elems| {
defer self.l.alloc.free(elems);
for (elems) |t| param_ids.append(self.l.alloc, t) catch return .unresolved;
const ret_ty = if (ct.return_type) |rt| self.l.resolveTypeWithBindings(rt) else .void;
return self.l.module.types.closureType(param_ids.items, ret_ty);
}
if (self.l.pack_bindings) |pb| {
if (pb.get(pn)) |pack_tys| {
for (pack_tys) |t| param_ids.append(self.l.alloc, t) catch return .unresolved;
// Fully bound — emit a concrete closure type, no pack_start.
const ret_ty = if (ct.return_type) |rt| self.l.resolveTypeWithBindings(rt) else .void;
return self.l.module.types.closureType(param_ids.items, ret_ty);
}
}
// Pack name in scope but no binding — preserve the pack-shape
// so downstream code can still see it's variadic. (Hit during
// impl-block parsing before any concrete monomorphisation.)
const ret_ty = if (ct.return_type) |rt| self.l.resolveTypeWithBindings(rt) else .void;
return self.l.module.types.closureTypePack(param_ids.items, ret_ty, @intCast(param_ids.items.len));
}
const ret_ty = if (ct.return_type) |rt| self.l.resolveTypeWithBindings(rt) else .void;
return self.l.module.types.closureType(param_ids.items, ret_ty);
}
/// Resolve a tuple type expression with active pack bindings: a spread field
/// `(..xs)` / `(..xs.T)` expands to the pack's per-element types via
/// `packTypeElems`. Non-spread fields resolve normally.
pub fn resolveTupleTypeWithBindings(self: PackResolver, tt: *const ast.TupleTypeExpr) TypeId {
var field_ids = std.ArrayList(TypeId).empty;
defer field_ids.deinit(self.l.alloc);
var had_spread = false;
for (tt.field_types) |ft| {
if (ft.data == .spread_expr) {
if (self.packTypeElems(ft.data.spread_expr.operand)) |elems| {
defer self.l.alloc.free(elems);
for (elems) |e| field_ids.append(self.l.alloc, e) catch return .unresolved;
had_spread = true;
continue;
}
}
field_ids.append(self.l.alloc, self.l.resolveTypeWithBindings(ft)) catch return .unresolved;
}
// Preserve field names for a named tuple `(x: T, y: U)` so `t.x` resolves
// (matches type_bridge.resolveTupleType). A spread expands to unnamed
// pack elements, so names only apply when there was no spread.
var name_ids: ?[]const types.StringId = null;
if (!had_spread) {
if (tt.field_names) |names| {
if (names.len == field_ids.items.len) {
var ids = std.ArrayList(types.StringId).empty;
for (names) |n| ids.append(self.l.alloc, self.l.module.types.internString(n)) catch return .unresolved;
name_ids = ids.toOwnedSlice(self.l.alloc) catch null;
}
}
}
return self.l.module.types.intern(.{ .tuple = .{
.fields = self.l.alloc.dupe(TypeId, field_ids.items) catch return .unresolved,
.names = name_ids,
} });
}
/// 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;
}
}
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
/// per-element types. `..xs` → the pack's element types (`pack_arg_types`).
/// `..xs.T` → each element's protocol type-arg `T` (from its
/// `impl P(args) for elem` in `param_impl_map`). Null when not a pack spread.
/// Caller owns the returned slice.
pub fn packTypeElems(self: PackResolver, operand: *const Node) ?[]TypeId {
const pat = self.l.pack_arg_types orelse return null;
// `..F(Ts)` — apply a parameterized type `F` to each pack element:
// `(..VL(Ts))` → `(VL(T0), VL(T1), …)`. Per element, temporarily bind
// the pack name to that single element type and resolve `F(elem)`.
if (operand.data == .parameterized_type_expr) {
const pt = operand.data.parameterized_type_expr;
var pack_name_p: []const u8 = "";
for (pt.args) |a| {
const nm = switch (a.data) {
.identifier => |id| id.name,
.type_expr => |te| te.name,
else => continue,
};
if (pat.contains(nm)) {
pack_name_p = nm;
break;
}
}
if (pack_name_p.len == 0) return null;
const elems = pat.get(pack_name_p) orelse return null;
if (self.l.type_bindings == null) return null;
var out = std.ArrayList(TypeId).empty;
for (elems) |ti| {
const had = self.l.type_bindings.?.get(pack_name_p);
self.l.type_bindings.?.put(pack_name_p, ti) catch {};
out.append(self.l.alloc, self.l.resolveTypeWithBindings(operand)) catch return null;
if (had) |h| self.l.type_bindings.?.put(pack_name_p, h) catch {} else _ = self.l.type_bindings.?.remove(pack_name_p);
}
return out.toOwnedSlice(self.l.alloc) catch null;
}
// In type position `xs` / `xs.T` parse to a (possibly dotted) type_expr
// name; `field_access` covers any value-shaped form.
var pack_name: []const u8 = "";
var projection: ?[]const u8 = null;
switch (operand.data) {
.type_expr, .identifier => {
const full = if (operand.data == .type_expr) operand.data.type_expr.name else operand.data.identifier.name;
if (std.mem.indexOfScalar(u8, full, '.')) |dot| {
pack_name = full[0..dot];
projection = full[dot + 1 ..];
} else {
pack_name = full;
}
},
.field_access => |fa| {
pack_name = switch (fa.object.data) {
.identifier => |id| id.name,
.type_expr => |te| te.name,
else => return null,
};
projection = fa.field;
},
else => return null,
}
return self.packTypeArgs(pack_name, projection);
}
/// Per-element types for a bound protocol pack: `pack_name` alone → the
/// element types; with `projection` (`xs.T`) → each element's protocol
/// type-arg. Null when `pack_name` isn't a bound pack. Caller owns the slice.
pub fn packTypeArgs(self: PackResolver, pack_name: []const u8, projection: ?[]const u8) ?[]TypeId {
const pat = self.l.pack_arg_types orelse return null;
const elems = pat.get(pack_name) orelse return null;
if (projection == null) return self.l.alloc.dupe(TypeId, elems) catch null;
const proto = if (self.l.pack_constraint) |pc| (pc.get(pack_name) orelse return null) else return null;
const arg_idx = self.l.lookupProtocolArg(proto, projection.?) orelse return null;
var out = std.ArrayList(TypeId).empty;
for (elems) |elem| {
const proj_ty = self.elementProtocolTypeArg(proto, elem, arg_idx) orelse blk: {
// The projection named a protocol type-arg this element's impl
// does not provide — there is no type for the slot. Surface it
// loudly: a diagnostic plus the `.unresolved` sentinel (a real
// `.void` here would read as a legitimate type downstream and
// silently corrupt the pack).
if (self.l.diagnostics) |diags| {
diags.addFmt(.err, null, "pack projection '{s}.{s}' has no type for a pack element: no matching `impl {s}(...) for {s}`", .{
pack_name, projection.?, proto, self.l.mangleTypeName(elem),
});
}
break :blk .unresolved;
};
out.append(self.l.alloc, proj_ty) catch return null;
}
return out.toOwnedSlice(self.l.alloc) catch null;
}
/// For a concrete `elem` conforming to parameterised `proto`, return the
/// `arg_idx`-th protocol type-arg from its `impl proto(args) for elem`
/// (scans `param_impl_map` for `proto\x00…\x00mangle(elem)`).
pub fn elementProtocolTypeArg(self: PackResolver, proto: []const u8, elem: TypeId, arg_idx: u32) ?TypeId {
const prefix = std.fmt.allocPrint(self.l.alloc, "{s}\x00", .{proto}) catch return null;
const suffix = std.fmt.allocPrint(self.l.alloc, "\x00{s}", .{self.l.mangleTypeName(elem)}) catch return null;
var it = self.l.param_impl_map.iterator();
while (it.next()) |entry| {
const k = entry.key_ptr.*;
if (std.mem.startsWith(u8, k, prefix) and std.mem.endsWith(u8, k, suffix)) {
for (entry.value_ptr.items) |impl| {
if (arg_idx < impl.target_args.len) return impl.target_args[arg_idx];
}
}
}
return null;
}
};

View File

@@ -74,7 +74,8 @@ pub const ProgramIndex = struct {
foreign_class_map: std.StringHashMap(*const ast.ForeignClassDecl) = std.StringHashMap(*const ast.ForeignClassDecl).init(std.heap.page_allocator),
/// `#run` global name → GlobalId.
global_names: std.StringHashMap(GlobalInfo),
/// Type alias name → target TypeId. Loaned to `TypeTable.aliases`.
/// Type alias name → target TypeId. The single-source alias table; passed
/// explicitly to `TypeResolver` / `type_bridge` resolution (no borrow).
type_alias_map: std.StringHashMap(TypeId) = std.StringHashMap(TypeId).init(std.heap.page_allocator),
/// Generic struct name → template.
struct_template_map: std.StringHashMap(StructTemplate) = std.StringHashMap(StructTemplate).init(std.heap.page_allocator),

View File

@@ -14,13 +14,13 @@ test "bridgeType: primitives" {
var table = TypeTable.init(alloc);
defer table.deinit();
try std.testing.expectEqual(TypeId.s32, type_bridge.bridgeType(.{ .signed = 32 }, &table));
try std.testing.expectEqual(TypeId.u8, type_bridge.bridgeType(.{ .unsigned = 8 }, &table));
try std.testing.expectEqual(TypeId.f64, type_bridge.bridgeType(.f64, &table));
try std.testing.expectEqual(TypeId.void, type_bridge.bridgeType(.void_type, &table));
try std.testing.expectEqual(TypeId.bool, type_bridge.bridgeType(.boolean, &table));
try std.testing.expectEqual(TypeId.string, type_bridge.bridgeType(.string_type, &table));
try std.testing.expectEqual(TypeId.any, type_bridge.bridgeType(.any_type, &table));
try std.testing.expectEqual(TypeId.s32, type_bridge.bridgeType(.{ .signed = 32 }, &table, null));
try std.testing.expectEqual(TypeId.u8, type_bridge.bridgeType(.{ .unsigned = 8 }, &table, null));
try std.testing.expectEqual(TypeId.f64, type_bridge.bridgeType(.f64, &table, null));
try std.testing.expectEqual(TypeId.void, type_bridge.bridgeType(.void_type, &table, null));
try std.testing.expectEqual(TypeId.bool, type_bridge.bridgeType(.boolean, &table, null));
try std.testing.expectEqual(TypeId.string, type_bridge.bridgeType(.string_type, &table, null));
try std.testing.expectEqual(TypeId.any, type_bridge.bridgeType(.any_type, &table, null));
}
test "bridgeType: composite types" {
@@ -29,19 +29,19 @@ test "bridgeType: composite types" {
defer table.deinit();
// Pointer
const ptr_id = type_bridge.bridgeType(.{ .pointer_type = .{ .pointee_name = "s32" } }, &table);
const ptr_id = type_bridge.bridgeType(.{ .pointer_type = .{ .pointee_name = "s32" } }, &table, null);
try std.testing.expectEqual(TypeInfo{ .pointer = .{ .pointee = .s32 } }, table.get(ptr_id));
// Slice
const slice_id = type_bridge.bridgeType(.{ .slice_type = .{ .element_name = "u8" } }, &table);
const slice_id = type_bridge.bridgeType(.{ .slice_type = .{ .element_name = "u8" } }, &table, null);
try std.testing.expectEqual(TypeInfo{ .slice = .{ .element = .u8 } }, table.get(slice_id));
// Array
const arr_id = type_bridge.bridgeType(.{ .array_type = .{ .element_name = "f32", .length = 4 } }, &table);
const arr_id = type_bridge.bridgeType(.{ .array_type = .{ .element_name = "f32", .length = 4 } }, &table, null);
try std.testing.expectEqual(TypeInfo{ .array = .{ .element = .f32, .length = 4 } }, table.get(arr_id));
// Optional
const opt_id = type_bridge.bridgeType(.{ .optional_type = .{ .child_name = "s64" } }, &table);
const opt_id = type_bridge.bridgeType(.{ .optional_type = .{ .child_name = "s64" } }, &table, null);
try std.testing.expectEqual(TypeInfo{ .optional = .{ .child = .s64 } }, table.get(opt_id));
}
@@ -54,7 +54,7 @@ test "resolveAstType: primitive type_expr" {
defer alloc.destroy(node);
node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "f64" } } };
try std.testing.expectEqual(TypeId.f64, type_bridge.resolveAstType(node, &table));
try std.testing.expectEqual(TypeId.f64, type_bridge.resolveAstType(node, &table, null));
}
test "resolveAstType: pointer type" {
@@ -70,7 +70,7 @@ test "resolveAstType: pointer type" {
defer alloc.destroy(node);
node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .pointer_type_expr = .{ .pointee_type = inner } } };
const id = type_bridge.resolveAstType(node, &table);
const id = type_bridge.resolveAstType(node, &table, null);
try std.testing.expectEqual(TypeInfo{ .pointer = .{ .pointee = .s32 } }, table.get(id));
}
@@ -91,7 +91,7 @@ test "resolveAstType: optional slice" {
defer alloc.destroy(opt);
opt.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .optional_type_expr = .{ .inner_type = slice } } };
const id = type_bridge.resolveAstType(opt, &table);
const id = type_bridge.resolveAstType(opt, &table, null);
const info = table.get(id);
switch (info) {
.optional => |o| {
@@ -107,52 +107,48 @@ test "resolveAstType: null surfaces as .unresolved (no silent s64 default)" {
var table = TypeTable.init(alloc);
defer table.deinit();
try std.testing.expectEqual(TypeId.unresolved, type_bridge.resolveAstType(null, &table));
try std.testing.expectEqual(TypeId.unresolved, type_bridge.resolveAstType(null, &table, null));
}
test "resolveAstType: TypeTable.aliases resolves named alias" {
test "resolveAstType: threaded alias_map resolves named alias" {
const alloc = std.testing.allocator;
var table = TypeTable.init(alloc);
defer table.deinit();
// No alias set yet — "ShaderHandle" is an unknown name; the
// resolver creates an empty struct stub (this is the silent-fail
// shape the alias borrow is here to fix).
// No alias map — "ShaderHandle" is an unknown name; the resolver creates
// an empty struct stub (this is the silent-fail shape the alias map fixes).
const sh_node = try alloc.create(Node);
defer alloc.destroy(sh_node);
sh_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "ShaderHandle" } } };
const empty_stub = type_bridge.resolveAstType(sh_node, &table);
const empty_stub = type_bridge.resolveAstType(sh_node, &table, null);
const empty_info = table.get(empty_stub);
try std.testing.expectEqual(@as(std.meta.Tag(TypeInfo), .@"struct"), std.meta.activeTag(empty_info));
try std.testing.expectEqual(@as(usize, 0), empty_info.@"struct".fields.len);
// Set up the alias map borrow. The previously-stubbed name now
// resolves to the alias target instead of a fresh stub.
// With an explicit alias map (threaded, not borrowed via a TypeTable field),
// a previously-unseen name resolves to the alias target instead of a stub.
var aliases = std.StringHashMap(TypeId).init(alloc);
defer aliases.deinit();
try aliases.put("ShaderHandle", .u32);
table.aliases = &aliases;
// Names already interned as stubs short-circuit on `findByName`
// — that's the existing behaviour. Use a FRESH alias name to
// demonstrate the new path's effect.
// Names already interned as stubs short-circuit on `findByName` — that's
// the existing behaviour. Use a FRESH alias name to demonstrate the path.
const opaque_node = try alloc.create(Node);
defer alloc.destroy(opaque_node);
opaque_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "Opaque" } } };
try aliases.put("Opaque", .u64);
try std.testing.expectEqual(TypeId.u64, type_bridge.resolveAstType(opaque_node, &table));
try std.testing.expectEqual(TypeId.u64, type_bridge.resolveAstType(opaque_node, &table, &aliases));
// Compound forms (`*Opaque`, `[]Opaque`, `?Opaque`) route
// through recursive helpers that ultimately re-enter
// `resolveTypeName` — the alias map is consulted every step.
// Compound forms (`*Opaque`, `[]Opaque`, `?Opaque`) route through recursive
// helpers that thread the same alias_map at every step.
const opaque_inner = try alloc.create(Node);
defer alloc.destroy(opaque_inner);
opaque_inner.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "Opaque" } } };
const ptr_node = try alloc.create(Node);
defer alloc.destroy(ptr_node);
ptr_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .pointer_type_expr = .{ .pointee_type = opaque_inner } } };
const ptr_id = type_bridge.resolveAstType(ptr_node, &table);
const ptr_id = type_bridge.resolveAstType(ptr_node, &table, &aliases);
try std.testing.expectEqual(TypeInfo{ .pointer = .{ .pointee = .u64 } }, table.get(ptr_id));
}
@@ -169,7 +165,7 @@ test "resolveAstType: error_set_decl registers an error-set type + interns tags"
.tag_names = &tag_names,
} } };
const id = type_bridge.resolveAstType(node, &table);
const id = type_bridge.resolveAstType(node, &table, null);
const info = table.get(id);
try std.testing.expect(info == .error_set);
try std.testing.expectEqualStrings("ParseErr", table.getString(info.error_set.name));
@@ -177,7 +173,7 @@ test "resolveAstType: error_set_decl registers an error-set type + interns tags"
// Tags were interned into the global pool (round-trip a name through it).
try std.testing.expectEqualStrings("BadDigit", table.getTagName(table.internTag("BadDigit")));
// Re-resolving the same decl dedups to the same TypeId.
try std.testing.expectEqual(id, type_bridge.resolveAstType(node, &table));
try std.testing.expectEqual(id, type_bridge.resolveAstType(node, &table, null));
}
// ── ERR E1.2 — failable-signature error channel resolution ──
@@ -194,7 +190,7 @@ test "resolveAstType: `!Named` resolves to the declared error set" {
const node = try alloc.create(Node);
defer alloc.destroy(node);
node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .error_type_expr = .{ .name = "ParseErr" } } };
try std.testing.expectEqual(set, type_bridge.resolveAstType(node, &table));
try std.testing.expectEqual(set, type_bridge.resolveAstType(node, &table, null));
}
test "resolveAstType: bare `!` resolves to a shared inferred placeholder set" {
@@ -209,8 +205,8 @@ test "resolveAstType: bare `!` resolves to a shared inferred placeholder set" {
defer alloc.destroy(b);
b.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .error_type_expr = .{ .name = null } } };
const ia = type_bridge.resolveAstType(a, &table);
const ib = type_bridge.resolveAstType(b, &table);
const ia = type_bridge.resolveAstType(a, &table, null);
const ib = type_bridge.resolveAstType(b, &table, null);
try std.testing.expect(table.get(ia) == .error_set);
try std.testing.expectEqualStrings("!", table.getString(table.get(ia).error_set.name));
try std.testing.expectEqual(@as(usize, 0), table.get(ia).error_set.tags.len); // empty until E1.4 SCC
@@ -238,7 +234,7 @@ test "resolveAstType: `(s32, !Named)` result list is a tuple ending in the error
defer alloc.destroy(tuple);
tuple.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .tuple_type_expr = .{ .field_types = &fields, .field_names = null } } };
const id = type_bridge.resolveAstType(tuple, &table);
const id = type_bridge.resolveAstType(tuple, &table, null);
const info = table.get(id);
try std.testing.expect(info == .tuple);
try std.testing.expectEqual(@as(usize, 2), info.tuple.fields.len);

View File

@@ -8,29 +8,65 @@ const TypeId = ir_types.TypeId;
const TypeInfo = ir_types.TypeInfo;
const TypeTable = ir_types.TypeTable;
const StringId = ir_types.StringId;
const type_resolver = @import("type_resolver.zig");
/// The single-source type-alias table (`ProgramIndex.type_alias_map`), threaded
/// explicitly through every name-resolving entry point so a bare name like
/// `ShaderHandle` (declared `ShaderHandle :: u32`) resolves to its target
/// rather than a fresh empty-struct stub. Replaces the old `TypeTable.aliases`
/// borrow (A2.3): there is no hidden alias state — callers pass the map (or
/// `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).
pub fn resolveAstType(node: ?*const Node, table: *TypeTable) TypeId {
pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap) TypeId {
// A null node means a caller reached type resolution without a type node.
// Every current caller either passes a non-optional node or handles the
// "no type" case itself (returning `.void`), so this is a caller bug — and
// `.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),
.identifier => |id| resolveTypeName(id.name, table),
.array_type_expr => |at| resolveArrayType(&at, table),
.slice_type_expr => |st| resolveSliceType(&st, table),
.pointer_type_expr => |pt| resolvePointerType(&pt, table),
.many_pointer_type_expr => |mpt| resolveManyPointerType(&mpt, table),
.optional_type_expr => |ot| resolveOptionalType(&ot, table),
.function_type_expr => |ft| resolveFunctionType(&ft, table),
.closure_type_expr => |ct| resolveClosureType(&ct, table),
.tuple_type_expr => |tt| resolveTupleType(&tt, table),
.type_expr => |te| resolveTypeName(te.name, table, alias_map),
.identifier => |id| resolveTypeName(id.name, 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
@@ -43,8 +79,8 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable) TypeId {
std.debug.print("type_bridge: pack-index type expression encountered outside a pack-aware context — returning .unresolved\n", .{});
return .unresolved;
},
.tuple_literal => |tl| resolveTupleLiteralAsType(&tl, table),
.parameterized_type_expr => |pt| resolveParameterizedType(&pt, table),
.tuple_literal => |tl| resolveTupleLiteralAsType(&tl, table, alias_map),
.parameterized_type_expr => |pt| resolveParameterizedType(&pt, table, alias_map),
// An unannotated param. Its type must be resolved from context
// (contextual closure typing, generic binding, or pack substitution)
// *before* reaching here; if it doesn't, returning a plausible `.s64`
@@ -54,11 +90,11 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable) TypeId {
// turns it into a real diagnostic.
.inferred_type => .unresolved,
// Inline type declarations (used as field types)
.enum_decl => |ed| resolveInlineEnum(&ed, table),
.struct_decl => |sd| resolveInlineStruct(&sd, table),
.union_decl => |ud| resolveInlineUnion(&ud, table),
.enum_decl => |ed| resolveInlineEnum(&ed, table, alias_map),
.struct_decl => |sd| resolveInlineStruct(&sd, table, alias_map),
.union_decl => |ud| resolveInlineUnion(&ud, table, alias_map),
.error_set_decl => |esd| resolveInlineErrorSet(&esd, table),
.error_type_expr => |ete| resolveErrorType(&ete, table),
.error_type_expr => |ete| resolveErrorType(&ete, table, alias_map),
else => {
// A non-type AST node reached type resolution — a caller bug.
// Returning a plausible `.s64` would silently fabricate an 8-byte
@@ -74,7 +110,7 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable) TypeId {
// Translate an existing codegen Type value into an IR TypeId. Used when
// we have access to the codegen's resolved type info (Phase 3+).
pub fn bridgeType(ty: sx_types.Type, table: *TypeTable) TypeId {
pub fn bridgeType(ty: sx_types.Type, table: *TypeTable, alias_map: AliasMap) TypeId {
return switch (ty) {
.signed => |w| switch (w) {
8 => .s8,
@@ -104,52 +140,52 @@ pub fn bridgeType(ty: sx_types.Type, table: *TypeTable) TypeId {
.struct_type => |name| resolveNamedType(name, .@"struct", table),
.union_type => |name| resolveNamedType(name, .@"union", table),
.array_type => |info| blk: {
const elem = resolveTypeName(info.element_name, table);
const elem = resolveTypeName(info.element_name, table, alias_map);
break :blk table.arrayOf(elem, info.length);
},
.slice_type => |info| blk: {
const elem = resolveTypeName(info.element_name, table);
const elem = resolveTypeName(info.element_name, table, alias_map);
break :blk table.sliceOf(elem);
},
.pointer_type => |info| blk: {
const pointee = resolveTypeName(info.pointee_name, table);
const pointee = resolveTypeName(info.pointee_name, table, alias_map);
break :blk table.ptrTo(pointee);
},
.many_pointer_type => |info| blk: {
const elem = resolveTypeName(info.element_name, table);
const elem = resolveTypeName(info.element_name, table, alias_map);
break :blk table.manyPtrTo(elem);
},
.optional_type => |info| blk: {
const child = resolveTypeName(info.child_name, table);
const child = resolveTypeName(info.child_name, table, alias_map);
break :blk table.optionalOf(child);
},
.vector_type => |info| blk: {
const elem = resolveTypeName(info.element_name, table);
const elem = resolveTypeName(info.element_name, table, alias_map);
break :blk table.vectorOf(elem, info.length);
},
.function_type => |info| blk: {
const alloc = table.alloc;
var param_ids = std.ArrayList(TypeId).empty;
for (info.param_types) |pt| {
param_ids.append(alloc, bridgeType(pt, table)) catch unreachable;
param_ids.append(alloc, bridgeType(pt, table, alias_map)) catch unreachable;
}
const ret_id = bridgeType(info.return_type.*, table);
const ret_id = bridgeType(info.return_type.*, table, alias_map);
break :blk table.functionType(param_ids.items, ret_id);
},
.closure_type => |info| blk: {
const alloc = table.alloc;
var param_ids = std.ArrayList(TypeId).empty;
for (info.param_types) |pt| {
param_ids.append(alloc, bridgeType(pt, table)) catch unreachable;
param_ids.append(alloc, bridgeType(pt, table, alias_map)) catch unreachable;
}
const ret_id = bridgeType(info.return_type.*, table);
const ret_id = bridgeType(info.return_type.*, table, alias_map);
break :blk table.closureType(param_ids.items, ret_id);
},
.tuple_type => |info| blk: {
const alloc = table.alloc;
var field_ids = std.ArrayList(TypeId).empty;
for (info.field_types) |ft| {
field_ids.append(alloc, bridgeType(ft, table)) catch unreachable;
field_ids.append(alloc, bridgeType(ft, table, alias_map)) catch unreachable;
}
var name_ids: ?[]const StringId = null;
if (info.field_names) |names| {
@@ -186,162 +222,44 @@ fn resolveNamedType(name: []const u8, kind: NamedKind, table: *TypeTable) TypeId
};
}
fn resolveTypeName(name: []const u8, table: *TypeTable) TypeId {
// Try primitive first
if (resolveTypePrimitive(name)) |id| return id;
// Arbitrary bit-width integers: s1-s64, u1-u64
if (name.len >= 2 and (name[0] == 's' or name[0] == 'u')) {
if (std.fmt.parseInt(u8, name[1..], 10)) |width| {
if (width >= 1 and width <= 64) {
if (name[0] == 's') {
return table.intern(.{ .signed = width });
} else {
return table.intern(.{ .unsigned = width });
}
}
} else |_| {}
}
// Sentinel-terminated slice: [:0]u8 → string
if (name.len >= 5 and name[0] == '[' and name[1] == ':') {
if (std.mem.indexOfScalar(u8, name, ']')) |close| {
const sentinel = name[2..close];
const elem = name[close + 1 ..];
if (std.mem.eql(u8, sentinel, "0") and std.mem.eql(u8, elem, "u8")) {
return .string;
}
}
}
// Many-pointer: [*]T
if (name.len >= 4 and name[0] == '[' and name[1] == '*' and name[2] == ']') {
const elem = resolveTypeName(name[3..], table);
return table.manyPtrTo(elem);
}
// Pointer: *T
if (name.len >= 2 and name[0] == '*') {
const pointee = resolveTypeName(name[1..], table);
return table.ptrTo(pointee);
}
// Optional: ?T
if (name.len >= 2 and name[0] == '?') {
const child = resolveTypeName(name[1..], table);
return table.optionalOf(child);
}
// Assume it's a named struct/enum/union type
const name_id = table.internString(name);
// Check if already registered (e.g., as a union from enum_decl)
if (table.findByName(name_id)) |existing| return existing;
// Type alias defined elsewhere (e.g. `ShaderHandle :: u32`,
// `Vec4 :: Vector(4, f32)`) — resolve via the borrowed alias
// map before falling through to the empty-struct stub. Without
// this, the name is silently interned as a fresh empty struct
// and downstream IR emits `{}` parameters / fields. The
// previous fix lived per-call-site in lower.zig (protocol decl,
// resolveTypeWithBindings); centralising it here means struct
// fields, var annotations, function signatures, and every other
// resolveAstType caller all pick up the same resolution.
if (table.aliases) |amap| {
if (amap.get(name)) |alias_ty| return alias_ty;
}
return table.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } });
/// Resolve a bare type name. The algorithm lives in `type_resolver.zig`
/// (`TypeResolver.resolveNamed`, the single source); `type_bridge` forwards the
/// caller-threaded `alias_map` (the single-source `ProgramIndex.type_alias_map`).
fn resolveTypeName(name: []const u8, table: *TypeTable, alias_map: AliasMap) TypeId {
return type_resolver.TypeResolver.resolveNamed(name, table, alias_map);
}
pub fn resolveTypePrimitive(name: []const u8) ?TypeId {
if (name.len == 0) return null;
// Fast path for common types
if (std.mem.eql(u8, name, "s64")) return .s64;
if (std.mem.eql(u8, name, "s32")) return .s32;
if (std.mem.eql(u8, name, "s16")) return .s16;
if (std.mem.eql(u8, name, "s8")) return .s8;
if (std.mem.eql(u8, name, "u64")) return .u64;
if (std.mem.eql(u8, name, "u32")) return .u32;
if (std.mem.eql(u8, name, "u16")) return .u16;
if (std.mem.eql(u8, name, "u8")) return .u8;
if (std.mem.eql(u8, name, "f32")) return .f32;
if (std.mem.eql(u8, name, "f64")) return .f64;
if (std.mem.eql(u8, name, "bool")) return .bool;
if (std.mem.eql(u8, name, "string")) return .string;
if (std.mem.eql(u8, name, "void")) return .void;
if (std.mem.eql(u8, name, "Any")) return .any;
// Type values are runtime-representable as Any-shaped pairs:
// `{ tag = .any.index() (the meta-marker), value = TypeId.index() }`.
// Lets `t : Type = f64; t == s64; print(t)` all route through the
// existing Any infrastructure — boxing/unboxing, `case type:`
// dispatch, runtime `type_name(t)` via the type-name lookup
// table. Comparison decomposes via the eq fold path
// (`isStaticTypeRef`) for static literals; runtime-var vs
// literal compares decompose at `lowerBinaryOp`.
if (std.mem.eql(u8, name, "Type")) return .any;
if (std.mem.eql(u8, name, "noreturn")) return .noreturn;
if (std.mem.eql(u8, name, "usize")) return .usize;
if (std.mem.eql(u8, name, "isize")) return .isize;
return null;
}
/// Builtin primitive keyword → TypeId. The keyword table now lives in
/// `type_resolver.zig` (architecture phase A2.1, `TypeResolver.resolvePrimitive`);
/// re-exported here so existing callers are unaffected while `type_bridge` is
/// 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) TypeId {
const elem = resolveAstType(at.element_type, table);
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) TypeId {
const elem = resolveAstType(st.element_type, table);
return table.sliceOf(elem);
}
fn resolvePointerType(pt: *const ast.PointerTypeExpr, table: *TypeTable) TypeId {
const pointee = resolveAstType(pt.pointee_type, table);
return table.ptrTo(pointee);
}
fn resolveManyPointerType(mpt: *const ast.ManyPointerTypeExpr, table: *TypeTable) TypeId {
const elem = resolveAstType(mpt.element_type, table);
return table.manyPtrTo(elem);
}
fn resolveOptionalType(ot: *const ast.OptionalTypeExpr, table: *TypeTable) TypeId {
const child = resolveAstType(ot.inner_type, table);
return table.optionalOf(child);
}
fn resolveFunctionType(ft: *const ast.FunctionTypeExpr, table: *TypeTable) TypeId {
const alloc = table.alloc;
var param_ids = std.ArrayList(TypeId).empty;
for (ft.param_types) |pt| {
param_ids.append(alloc, resolveAstType(pt, table)) catch unreachable;
}
const ret_id = if (ft.return_type) |rt| resolveAstType(rt, table) 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) 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)) catch unreachable;
param_ids.append(alloc, resolveAstType(pt, table, alias_map)) catch unreachable;
}
const ret_id = if (ct.return_type) |rt| resolveAstType(rt, table) 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);
const ret_id = if (ct.return_type) |rt| resolveAstType(rt, table, alias_map) else TypeId.void;
return table.closureTypePack(param_ids.items, ret_id, @intCast(param_ids.items.len));
}
fn resolveTupleType(tt: *const ast.TupleTypeExpr, table: *TypeTable) 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| {
field_ids.append(alloc, resolveAstType(ft, table)) catch unreachable;
field_ids.append(alloc, resolveAstType(ft, table, alias_map)) catch unreachable;
}
var name_ids: ?[]const StringId = null;
if (tt.field_names) |names| {
@@ -358,20 +276,23 @@ fn resolveTupleType(tt: *const ast.TupleTypeExpr, table: *TypeTable) TypeId {
}
// Treat a tuple value literal as the corresponding tuple TYPE — valid only when
// every element is itself a type expression. Non-type elements report a clear
// diagnostic and degrade to .s64 for that slot (which the snapshot will catch).
fn resolveTupleLiteralAsType(tl: *const ast.TupleLiteral, table: *TypeTable) TypeId {
// every element is itself a type expression. A non-type element (e.g. the `1`
// in `(s32, 1)`) means this literal is NOT a type: refuse to fabricate a tuple
// and return the `.unresolved` sentinel (never `.s64`, which would silently lie
// about the size — issue 0067). type_bridge is stateless and has no diagnostics;
// the user-facing diagnostic is emitted by the stateful caller
// (`Lowering.resolveTupleLiteralTypeArg`), which validates before delegating
// here, so the valid path below builds the tuple and the invalid path never
// reaches it from lowering. The sentinel is the backstop for any other
// (binding-free) caller.
fn resolveTupleLiteralAsType(tl: *const ast.TupleLiteral, table: *TypeTable, alias_map: AliasMap) TypeId {
const alloc = table.alloc;
var field_ids = std.ArrayList(TypeId).empty;
var name_ids_list = std.ArrayList(StringId).empty;
var any_named = false;
for (tl.elements) |el| {
if (!isTypeShapedAstNode(el.value, table)) {
std.debug.print("type_bridge: tuple literal element is not a type (tag={s}) — cannot use as tuple type\n", .{@tagName(el.value.data)});
field_ids.append(alloc, .s64) catch unreachable;
} else {
field_ids.append(alloc, resolveAstType(el.value, table)) catch unreachable;
}
if (!isTypeShapedAstNode(el.value, table)) return .unresolved;
field_ids.append(alloc, resolveAstType(el.value, table, alias_map)) catch unreachable;
if (el.name) |n| {
any_named = true;
name_ids_list.append(alloc, table.internString(n)) catch unreachable;
@@ -415,7 +336,7 @@ pub fn isTypeShapedAstNode(node: *const Node, table: *TypeTable) bool {
};
}
fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTable) TypeId {
fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId {
// Strip module prefix (e.g. "std.Vector" → "Vector")
const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name;
// Vector(N, T) is a built-in parameterized type
@@ -425,7 +346,7 @@ fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTa
.int_literal => |lit| @intCast(@as(u64, @bitCast(lit.value))),
else => 0,
};
const elem = resolveAstType(pt.args[1], table);
const elem = resolveAstType(pt.args[1], table, alias_map);
return table.vectorOf(elem, length);
}
}
@@ -436,7 +357,7 @@ fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTa
// ── Inline type declarations ─────────────────────────────────────────
fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable) TypeId {
fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: AliasMap) TypeId {
const alloc = table.alloc;
const name_id = table.internString(ed.name);
@@ -462,7 +383,7 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable) TypeId {
} else {
var sfields = std.ArrayList(TypeInfo.StructInfo.Field).empty;
for (sd.field_names, sd.field_types) |fname, ftype_node| {
const fty = resolveAstType(ftype_node, table);
const fty = resolveAstType(ftype_node, table, alias_map);
sfields.append(alloc, .{
.name = table.internString(fname),
.ty = fty,
@@ -476,10 +397,10 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable) TypeId {
table.update(field_ty, sinfo);
}
} else {
field_ty = resolveAstType(vt, table);
field_ty = resolveAstType(vt, table, alias_map);
}
} else {
field_ty = resolveAstType(vt, table);
field_ty = resolveAstType(vt, table, alias_map);
}
}
}
@@ -493,7 +414,7 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable) TypeId {
var backing_type: ?TypeId = null;
var tag_type: ?TypeId = null;
if (ed.backing_type) |bt| {
const backing_ty = resolveAstType(bt, table);
const backing_ty = resolveAstType(bt, table, alias_map);
backing_type = backing_ty;
// Extract tag type from first field of backing struct
const backing_info = table.get(backing_ty);
@@ -576,7 +497,7 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable) TypeId {
if (ed.backing_type) |bt| {
// Only use simple backing types (u8, u16, u32, etc.), not struct backing (enum struct)
if (bt.data != .struct_decl) {
enum_backing = resolveAstType(bt, table);
enum_backing = resolveAstType(bt, table, alias_map);
}
}
@@ -592,7 +513,7 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable) TypeId {
return id;
}
fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable) TypeId {
fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable, alias_map: AliasMap) TypeId {
const alloc = table.alloc;
const name_id = table.internString(sd.name);
@@ -600,7 +521,7 @@ fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable) TypeId {
var fields = std.ArrayList(TypeInfo.StructInfo.Field).empty;
for (sd.field_names, sd.field_types) |fname, ftype_node| {
const field_ty = resolveAstType(ftype_node, table);
const field_ty = resolveAstType(ftype_node, table, alias_map);
fields.append(alloc, .{
.name = table.internString(fname),
.ty = field_ty,
@@ -615,7 +536,7 @@ fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable) TypeId {
return id;
}
fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable) TypeId {
fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable, alias_map: AliasMap) TypeId {
const alloc = table.alloc;
const name_id = table.internString(ud.name);
@@ -623,7 +544,7 @@ fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable) TypeId {
var fields = std.ArrayList(TypeInfo.StructInfo.Field).empty;
for (ud.field_names, ud.field_types) |fname, ftype_node| {
const field_ty = resolveAstType(ftype_node, table);
const field_ty = resolveAstType(ftype_node, table, alias_map);
fields.append(alloc, .{
.name = table.internString(fname),
.ty = field_ty,
@@ -662,8 +583,8 @@ fn resolveInlineErrorSet(esd: *const ast.ErrorSetDecl, table: *TypeTable) TypeId
/// function by the whole-program SCC pass (E1.4); for now every bare `!`
/// resolves to the same empty inferred set, which is correct while no
/// function raises (E1.3+).
fn resolveErrorType(ete: *const ast.ErrorTypeExpr, table: *TypeTable) TypeId {
if (ete.name) |name| return resolveTypeName(name, table);
fn resolveErrorType(ete: *const ast.ErrorTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId {
if (ete.name) |name| return resolveTypeName(name, table, alias_map);
// `!` is not a legal type/identifier name, so this reserved StringId can
// never collide with a user-declared set.
const name_id = table.internString("!");

View File

@@ -0,0 +1,156 @@
const std = @import("std");
const ast = @import("../ast.zig");
const Node = ast.Node;
const types = @import("types.zig");
const TypeTable = types.TypeTable;
const TypeId = types.TypeId;
const tr_mod = @import("type_resolver.zig");
const TypeResolver = tr_mod.TypeResolver;
const ResolveEnv = tr_mod.ResolveEnv;
const ProgramIndex = @import("program_index.zig").ProgramIndex;
fn typeExpr(name: []const u8) Node {
return .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = name, .is_generic = false } } };
}
/// Stand-in for `Lowering.resolveInner`: the real hook recurses the full
/// stateful resolver; here element types are always primitives, resolved via
/// the keyword table.
const PrimInner = struct {
pub fn resolveInner(_: PrimInner, node: *const Node) TypeId {
return switch (node.data) {
.type_expr => |te| TypeResolver.resolvePrimitive(te.name) orelse .unresolved,
else => .unresolved,
};
}
};
test "TypeResolver.resolvePrimitive maps builtin keywords, null otherwise" {
try std.testing.expectEqual(@as(?TypeId, .s64), TypeResolver.resolvePrimitive("s64"));
try std.testing.expectEqual(@as(?TypeId, .bool), TypeResolver.resolvePrimitive("bool"));
try std.testing.expectEqual(@as(?TypeId, .f64), TypeResolver.resolvePrimitive("f64"));
try std.testing.expectEqual(@as(?TypeId, .void), TypeResolver.resolvePrimitive("void"));
try std.testing.expectEqual(@as(?TypeId, .any), TypeResolver.resolvePrimitive("Any"));
try std.testing.expectEqual(@as(?TypeId, .any), TypeResolver.resolvePrimitive("Type"));
try std.testing.expectEqual(@as(?TypeId, .usize), TypeResolver.resolvePrimitive("usize"));
try std.testing.expectEqual(@as(?TypeId, .isize), TypeResolver.resolvePrimitive("isize"));
try std.testing.expectEqual(@as(?TypeId, .noreturn), TypeResolver.resolvePrimitive("noreturn"));
// Non-primitives (aliases / generics / named structs) defer to the caller.
try std.testing.expect(TypeResolver.resolvePrimitive("List") == null);
try std.testing.expect(TypeResolver.resolvePrimitive("ShaderHandle") == null);
try std.testing.expect(TypeResolver.resolvePrimitive("") == null);
}
test "TypeResolver.resolveCompound builds structural compound types" {
// 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);
const inner = PrimInner{};
var s64n = typeExpr("s64");
var u8n = typeExpr("u8");
var f32n = typeExpr("f32");
var booln = typeExpr("bool");
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)), 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)), 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)), 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)), 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)), TypeResolver.resolveCompound(&table, &arr, inner));
// 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(TypeResolver.resolveCompound(&table, &name, inner) == null);
}
test "ResolveEnv default-constructs with all-null context" {
const env = ResolveEnv{};
try std.testing.expect(env.type_bindings == null);
try std.testing.expect(env.pack_bindings == null);
try std.testing.expect(env.target_type == null);
}
test "TypeResolver.resolveBinding reads ResolveEnv type bindings ($T)" {
const alloc = std.testing.allocator;
var tb = std.StringHashMap(TypeId).init(alloc);
defer tb.deinit();
try tb.put("T", .s64);
const env = ResolveEnv{ .type_bindings = &tb };
var bound = typeExpr("T");
try std.testing.expectEqual(@as(?TypeId, .s64), TypeResolver.resolveBinding(&bound, env));
// Unbound name → null (caller continues with primitive / alias / struct).
var unbound = typeExpr("U");
try std.testing.expect(TypeResolver.resolveBinding(&unbound, env) == null);
// No active bindings → null.
try std.testing.expect(TypeResolver.resolveBinding(&bound, ResolveEnv{}) == null);
}
test "TypeResolver.resolveName resolves aliases via ProgramIndex (not the TypeTable.aliases borrow)" {
const alloc = std.testing.allocator;
var table = TypeTable.init(alloc);
defer table.deinit();
var index = ProgramIndex.init(alloc);
defer index.deinit();
try index.type_alias_map.put("ShaderHandle", .u32); // alias → primitive
const ptr_s64 = table.ptrTo(.s64);
try index.type_alias_map.put("NodeRef", ptr_s64); // alias → pointer
const tr = TypeResolver{ .alloc = alloc, .types = &table, .diagnostics = null, .index = &index };
try std.testing.expectEqual(@as(TypeId, .u32), tr.resolveName("ShaderHandle"));
try std.testing.expectEqual(ptr_s64, tr.resolveName("NodeRef"));
// Primitive is checked before alias.
try std.testing.expectEqual(@as(TypeId, .s64), tr.resolveName("s64"));
}
test "TypeResolver.resolveNamed: width-int, string-prefix, unknown→stub" {
const alloc = std.testing.allocator;
var table = TypeTable.init(alloc);
defer table.deinit();
try std.testing.expectEqual(table.intern(.{ .signed = 7 }), TypeResolver.resolveNamed("s7", &table, null));
try std.testing.expectEqual(table.ptrTo(.s64), TypeResolver.resolveNamed("*s64", &table, null));
// Unknown name, no alias map → empty-struct stub (preserved behavior;
// never `.unresolved`, which is reserved for failed *generic* resolution).
try std.testing.expect(TypeResolver.resolveNamed("Unknown", &table, null) != .unresolved);
}

213
src/ir/type_resolver.zig Normal file
View File

@@ -0,0 +1,213 @@
const std = @import("std");
const ast = @import("../ast.zig");
const types = @import("types.zig");
const errors = @import("../errors.zig");
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):
/// the inputs that steer AST type-node resolution, replacing ad-hoc mutable
/// `Lowering` fields (`type_bindings`, `pack_*`, `comptime_value_bindings`,
/// `target_type`, …). A2.1 defines the shape; fields are consumed as later
/// phases move the cases that need them (generics/aliases A2.2, packs A2.3).
pub const ResolveEnv = struct {
type_bindings: ?*const std.StringHashMap(TypeId) = null,
pack_bindings: ?*const std.StringHashMap([]const TypeId) = null,
pack_arg_types: ?*const std.StringHashMap([]const TypeId) = null,
pack_constraints: ?*const std.StringHashMap([]const u8) = null,
comptime_values: ?*const std.StringHashMap(i64) = null,
target_type: ?TypeId = null,
};
/// Canonical AST-type-node → `TypeId` resolver (architecture phase A2). As of
/// A2.1 it owns the primitive-keyword table and the structural compound type
/// constructors. Later phases fold in generics/aliases (A2.2) and pack
/// projections (A2.3) and retire `src/ir/type_bridge.zig` (Principle 1).
///
/// Holds borrowed references only — constructed cheaply by value at each call
/// site (`Lowering.typeResolver()`), so it always reflects current state.
pub const TypeResolver = struct {
alloc: std.mem.Allocator,
types: *TypeTable,
diagnostics: ?*errors.DiagnosticList,
index: *ProgramIndex,
/// Builtin primitive keyword → `TypeId`; `null` for any non-primitive name
/// (the caller then continues with generic / alias / named-struct
/// resolution). Single source of truth for the builtin keyword set.
/// Namespaced (no `self`) — primitive resolution is stateless.
pub fn resolvePrimitive(name: []const u8) ?TypeId {
if (name.len == 0) return null;
if (std.mem.eql(u8, name, "s64")) return .s64;
if (std.mem.eql(u8, name, "s32")) return .s32;
if (std.mem.eql(u8, name, "s16")) return .s16;
if (std.mem.eql(u8, name, "s8")) return .s8;
if (std.mem.eql(u8, name, "u64")) return .u64;
if (std.mem.eql(u8, name, "u32")) return .u32;
if (std.mem.eql(u8, name, "u16")) return .u16;
if (std.mem.eql(u8, name, "u8")) return .u8;
if (std.mem.eql(u8, name, "f32")) return .f32;
if (std.mem.eql(u8, name, "f64")) return .f64;
if (std.mem.eql(u8, name, "bool")) return .bool;
if (std.mem.eql(u8, name, "string")) return .string;
if (std.mem.eql(u8, name, "void")) return .void;
if (std.mem.eql(u8, name, "Any")) return .any;
// `Type` values are runtime-representable as Any-shaped pairs
// `{ tag = .any.index(), value = TypeId.index() }`, so `Type` maps to
// `.any` and routes through the existing Any infrastructure.
if (std.mem.eql(u8, name, "Type")) return .any;
if (std.mem.eql(u8, name, "noreturn")) return .noreturn;
if (std.mem.eql(u8, name, "usize")) return .usize;
if (std.mem.eql(u8, name, "isize")) return .isize;
return null;
}
/// 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| 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 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,
};
}
/// Generic type-param binding lookup (`$T`, or a bare return-type `T`).
/// Reads the caller-supplied `ResolveEnv` rather than hidden `Lowering`
/// state. Returns null when there are no active bindings or the name is
/// unbound (the caller then continues with primitive / alias / struct
/// resolution, or returns `.unresolved` for an unbound generic `$R`).
pub fn resolveBinding(node: *const Node, env: ResolveEnv) ?TypeId {
const tb = env.type_bindings orelse return null;
return switch (node.data) {
.type_expr => |te| tb.get(te.name),
.identifier => |id| tb.get(id.name),
else => null,
};
}
/// Resolve a bare type NAME to a `TypeId`: primitive → arbitrary-width int
/// (`s1``u64`) → string-form pointer/slice/optional prefixes → already-
/// registered named type → alias (`alias_map`) → fresh empty-struct stub.
/// `alias_map` is the single-source alias table (owned by `ProgramIndex`);
/// callers pass it explicitly — Lowering via the index (`resolveName`),
/// `type_bridge` via the alias map threaded through `resolveAstType`. The
/// stub fall-through preserves long-standing behavior for as-yet-
/// unregistered names.
pub fn resolveNamed(name: []const u8, table: *TypeTable, alias_map: ?*const std.StringHashMap(TypeId)) TypeId {
if (resolvePrimitive(name)) |id| return id;
// Arbitrary bit-width integers: s1-s64, u1-u64.
if (name.len >= 2 and (name[0] == 's' or name[0] == 'u')) {
if (std.fmt.parseInt(u8, name[1..], 10)) |width| {
if (width >= 1 and width <= 64) {
return if (name[0] == 's') table.intern(.{ .signed = width }) else table.intern(.{ .unsigned = width });
}
} else |_| {}
}
// Sentinel-terminated slice: [:0]u8 → string.
if (name.len >= 5 and name[0] == '[' and name[1] == ':') {
if (std.mem.indexOfScalar(u8, name, ']')) |close| {
const sentinel = name[2..close];
const elem = name[close + 1 ..];
if (std.mem.eql(u8, sentinel, "0") and std.mem.eql(u8, elem, "u8")) return .string;
}
}
// Many-pointer: [*]T.
if (name.len >= 4 and name[0] == '[' and name[1] == '*' and name[2] == ']') {
return table.manyPtrTo(resolveNamed(name[3..], table, alias_map));
}
// Pointer: *T.
if (name.len >= 2 and name[0] == '*') {
return table.ptrTo(resolveNamed(name[1..], table, alias_map));
}
// Optional: ?T.
if (name.len >= 2 and name[0] == '?') {
return table.optionalOf(resolveNamed(name[1..], table, alias_map));
}
// Named struct/enum/union — already-registered wins, then alias, then
// a fresh empty-struct stub for an as-yet-unregistered name.
const name_id = table.internString(name);
if (table.findByName(name_id)) |existing| return existing;
if (alias_map) |amap| {
if (amap.get(name)) |alias_ty| return alias_ty;
}
return table.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } });
}
/// Resolve a bare type name through the canonical alias source
/// (`ProgramIndex.type_alias_map`).
pub fn resolveName(self: TypeResolver, name: []const u8) TypeId {
return resolveNamed(name, self.types, &self.index.type_alias_map);
}
};

View File

@@ -332,14 +332,6 @@ pub const TypeTable = struct {
slice_arena: std.heap.ArenaAllocator,
/// Target pointer size in bytes (4 for wasm32, 8 for 64-bit targets).
pointer_size: u8 = 8,
/// Borrowed pointer to `Lowering.program_index.type_alias_map`. When set,
/// `resolveTypeName` consults it before falling through to
/// the empty-struct-stub default — so a name like `ShaderHandle`
/// (defined `ShaderHandle :: u32`) resolves to `u32` rather than
/// being interned as a fresh empty struct. Pointer lifetime is
/// the owning Lowering's; consumers must clear it before the
/// Lowering is torn down.
aliases: ?*const std.StringHashMap(TypeId) = null,
pub fn init(alloc: Allocator) TypeTable {
var table = TypeTable{