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.
This commit is contained in:
agra
2026-06-02 13:56:32 +03:00
parent 9eb85cf9e3
commit dd16bab2c2
4 changed files with 147 additions and 94 deletions

View File

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

View File

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

View File

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

View File

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