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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user