diff --git a/examples/1116-diagnostics-tuple-type-nontype-element-rejected.sx b/examples/1116-diagnostics-tuple-type-nontype-element-rejected.sx new file mode 100644 index 0000000..e77e686 --- /dev/null +++ b/examples/1116-diagnostics-tuple-type-nontype-element-rejected.sx @@ -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 +} diff --git a/examples/expected/1116-diagnostics-tuple-type-nontype-element-rejected.exit b/examples/expected/1116-diagnostics-tuple-type-nontype-element-rejected.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1116-diagnostics-tuple-type-nontype-element-rejected.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1116-diagnostics-tuple-type-nontype-element-rejected.stderr b/examples/expected/1116-diagnostics-tuple-type-nontype-element-rejected.stderr new file mode 100644 index 0000000..4c0516e --- /dev/null +++ b/examples/expected/1116-diagnostics-tuple-type-nontype-element-rejected.stderr @@ -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))); + | ^ diff --git a/examples/expected/1116-diagnostics-tuple-type-nontype-element-rejected.stdout b/examples/expected/1116-diagnostics-tuple-type-nontype-element-rejected.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1116-diagnostics-tuple-type-nontype-element-rejected.stdout @@ -0,0 +1 @@ + diff --git a/issues/0067-tuple-literal-type-nontype-fallback.md b/issues/0067-tuple-literal-type-nontype-fallback.md new file mode 100644 index 0000000..66435ba --- /dev/null +++ b/issues/0067-tuple-literal-type-nontype-fallback.md @@ -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. diff --git a/src/ir/ir.zig b/src/ir/ir.zig index fa24b05..4e87576 100644 --- a/src/ir/ir.zig +++ b/src/ir/ir.zig @@ -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"); diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 416abde..4e6fd06 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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()`. - 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: `$[]` 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; } } diff --git a/src/ir/packs.test.zig b/src/ir/packs.test.zig new file mode 100644 index 0000000..a08f288 --- /dev/null +++ b/src/ir/packs.test.zig @@ -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 ` 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, "", ""); + 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()); +} diff --git a/src/ir/packs.zig b/src/ir/packs.zig new file mode 100644 index 0000000..7d65d96 --- /dev/null +++ b/src/ir/packs.zig @@ -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; + } +}; diff --git a/src/ir/program_index.zig b/src/ir/program_index.zig index 77ff9b7..a90d04c 100644 --- a/src/ir/program_index.zig +++ b/src/ir/program_index.zig @@ -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), diff --git a/src/ir/type_bridge.test.zig b/src/ir/type_bridge.test.zig index 26075ab..4b0312e 100644 --- a/src/ir/type_bridge.test.zig +++ b/src/ir/type_bridge.test.zig @@ -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); diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index 9594b73..c5662c5 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -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("!"); diff --git a/src/ir/type_resolver.test.zig b/src/ir/type_resolver.test.zig new file mode 100644 index 0000000..e16562f --- /dev/null +++ b/src/ir/type_resolver.test.zig @@ -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); +} diff --git a/src/ir/type_resolver.zig b/src/ir/type_resolver.zig new file mode 100644 index 0000000..2cb7166 --- /dev/null +++ b/src/ir/type_resolver.zig @@ -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); + } +}; diff --git a/src/ir/types.zig b/src/ir/types.zig index 2a7944a..be816d7 100644 --- a/src/ir/types.zig +++ b/src/ir/types.zig @@ -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{