From dd16bab2c21d90377e1fd3d59cc4aa34784d6e4b Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 2 Jun 2026 13:56:32 +0300 Subject: [PATCH] refactor(ir): move generic-binding + alias-aware name resolution into TypeResolver (A2.2) Architecture phase A2.2 -- behavior-preserving. TypeResolver gains the generic-binding and bare-name resolution it now owns: - resolveBinding(node, env): $T / bare return-type T lookup via an explicit ResolveEnv (no hidden Lowering state). - resolveNamed(name, table, alias_map): the full bare-name algorithm (primitive -> arbitrary-width int -> string-prefix [*]/*/?/[:0]u8 -> already-registered -> alias(alias_map) -> empty-struct stub), MOVED from type_bridge.resolveTypeName so it is single-sourced. - resolveName(self, name): resolves through the canonical alias source ProgramIndex.type_alias_map -- the compiler path no longer reads the TypeTable.aliases borrow. Lowering.resolveTypeWithBindings: the `if (self.type_bindings)` block (the $T lookup plus parameterized/call/closure/function arms that were redundant with the unconditional handling below) collapses to one resolveBinding delegation via a new resolveEnv() snapshot; the bare-name fallback routes type_expr/identifier to resolveName (index-based alias), other node kinds still to resolveAstType. type_bridge.resolveTypeName becomes a 1-line delegate to resolveNamed, passing its TypeTable.aliases borrow as the alias source. Single algorithm; the alias map stays single-sourced in ProgramIndex. Deferred to A2.3: removing the TypeTable.aliases borrow (its ~30 resolveAstType callers must converge onto TypeResolver first) and type_bridge's stateless compound resolvers. A2.2 #3 (templates/protocols/type-fns via ProgramIndex) was already satisfied by A1.1b. Tests: resolveBinding ($T bound/unbound/no-env), resolveName (alias->primitive, alias->pointer via ProgramIndex), resolveNamed (width-int, string-prefix, unknown->stub). No new fallback path; no duplicate truth. Gate green: zig build, zig build test, bash tests/run_examples.sh (350 passed, 0 failed). lower.zig 19372->19367; type_bridge.zig 647->592; type_resolver.zig 90->159. --- src/ir/lower.zig | 59 ++++++++++++++---------------- src/ir/type_bridge.zig | 69 ++++------------------------------- src/ir/type_resolver.test.zig | 44 ++++++++++++++++++++++ src/ir/type_resolver.zig | 69 +++++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 94 deletions(-) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index c0c9d33..05e4e46 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -20,6 +20,7 @@ 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 TypeId = types.TypeId; const StringId = types.StringId; @@ -12742,6 +12743,16 @@ pub const Lowering = struct { }; } + /// 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. @@ -12798,34 +12809,12 @@ pub const Lowering = struct { // stateful resolver (`resolveInner` → here) so generic structs / // bindings in element position keep their resolution. if (self.typeResolver().resolveCompound(node, self)) |t| return t; - 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; - }, - .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 => {}, - } - } + // 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); @@ -12885,10 +12874,16 @@ 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`) — this + // path no longer depends on the `TypeTable.aliases` borrow. Other node + // kinds (inline type decls, error types) still route through type_bridge + // (A2.3 converges its remaining `resolveAstType` callers). + switch (node.data) { + .type_expr => |te| return self.typeResolver().resolveName(te.name), + .identifier => |id| return self.typeResolver().resolveName(id.name), + else => return type_bridge.resolveAstType(node, &self.module.types), + } } /// Resolve a `Closure(...)` type expression with the active type/pack diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index 06bd203..d87d749 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -187,69 +187,14 @@ fn resolveNamedType(name: []const u8, kind: NamedKind, table: *TypeTable) TypeId }; } +/// Resolve a bare type name. The algorithm now lives in `type_resolver.zig` +/// (`TypeResolver.resolveNamed`, the single source); `type_bridge` passes its +/// `TypeTable.aliases` borrow as the alias source. The borrow remains the alias +/// access path for `type_bridge`'s remaining `resolveAstType` callers until +/// they are converged onto `TypeResolver` (A2.3); the alias map itself is +/// single-sourced in `ProgramIndex`. 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 = &.{} } }); + return type_resolver.TypeResolver.resolveNamed(name, table, table.aliases); } /// Builtin primitive keyword → TypeId. The keyword table now lives in diff --git a/src/ir/type_resolver.test.zig b/src/ir/type_resolver.test.zig index e911bf0..3d91de2 100644 --- a/src/ir/type_resolver.test.zig +++ b/src/ir/type_resolver.test.zig @@ -84,3 +84,47 @@ test "ResolveEnv default-constructs with all-null context" { 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 index a292560..e3714a5 100644 --- a/src/ir/type_resolver.zig +++ b/src/ir/type_resolver.zig @@ -87,4 +87,73 @@ pub const TypeResolver = struct { 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 its `TypeTable.aliases` borrow during the A2.3 + /// convergence. 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`) — the compiler path that no longer + /// depends on the `TypeTable.aliases` borrow. + pub fn resolveName(self: TypeResolver, name: []const u8) TypeId { + return resolveNamed(name, self.types, &self.index.type_alias_map); + } };