ir: generalize type-alias resolution via TypeTable.aliases borrow
Previously, type aliases (`ShaderHandle :: u32`, `Vec4 ::
Vector(4, f32)`) were resolved at three explicit call sites:
- `resolveTypeWithBindings` fallthrough (lower.zig: was 10481-83)
- Protocol method param resolution (was 11154-61)
- Protocol method return resolution (was 11169-76)
Every other `type_bridge.resolveAstType` caller silently fell into
`resolveTypeName`'s "create empty struct stub" path at the bottom,
materialising the alias name as a fresh `{Name=}` struct instead of
its target type. Symptom: the IR call signature got `{}` parameters
where the user meant `u32` etc.
This pushes the alias check inside `resolveTypeName` itself. A new
`TypeTable.aliases: ?*const std.StringHashMap(TypeId)` borrow is
loaned at `lowerRoot` from the owning Lowering. `resolveTypeName`
consults it before falling through to the stub default. Every
caller of `resolveAstType` (and its recursive helpers — `*Alias`,
`[]Alias`, `?Alias`, etc.) now picks up the same resolution.
The three pre-check sites in lower.zig collapse:
- `resolveTypeWithBindings`: the trailing alias pre-check is gone;
the comment now points at the new path.
- Protocol method param: the `Self → *void` short-circuit stays;
the alias arm is gone — the fallthrough handles it.
- Protocol method return: same shape.
Tests:
- `type_bridge.test.zig` gains `resolveAstType: TypeTable.aliases
resolves named alias` pinning the new behaviour. Demonstrates:
(1) no alias set → unknown name becomes empty struct stub (the
silent-fail shape we're fixing); (2) alias set → resolves to the
alias target; (3) compound forms (`*Alias`) recurse into
`resolveTypeName` for the inner name and pick up the alias.
224/224 example tests pass; zig build test green.
This commit is contained in:
@@ -280,6 +280,12 @@ 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.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.type_alias_map;
|
||||
const decls = switch (root.data) {
|
||||
.root => |r| r.decls,
|
||||
else => return,
|
||||
@@ -10472,10 +10478,9 @@ pub const Lowering = struct {
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
// Check type aliases before falling through to type_bridge
|
||||
if (node.data == .type_expr) {
|
||||
if (self.type_alias_map.get(node.data.type_expr.name)) |alias_ty| return alias_ty;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -11141,19 +11146,12 @@ pub const Lowering = struct {
|
||||
for (pd.methods) |method| {
|
||||
var ptypes = std.ArrayList(TypeId).empty;
|
||||
for (method.params) |p| {
|
||||
// Resolve param type; Self → *void for protocol context.
|
||||
// Type aliases (e.g. `ShaderHandle :: u32`) need to be
|
||||
// resolved through type_alias_map before falling through
|
||||
// to type_bridge — otherwise they're treated as named
|
||||
// empty structs and the LLVM call gets `{}` parameters.
|
||||
// Self → *void for protocol context; everything else
|
||||
// goes through `resolveAstType`, which now consults
|
||||
// the alias map via `TypeTable.aliases`.
|
||||
const pty = blk: {
|
||||
if (p.data == .type_expr) {
|
||||
if (std.mem.eql(u8, p.data.type_expr.name, "Self")) {
|
||||
break :blk void_ptr_ty;
|
||||
}
|
||||
if (self.type_alias_map.get(p.data.type_expr.name)) |aliased| {
|
||||
break :blk aliased;
|
||||
}
|
||||
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);
|
||||
};
|
||||
@@ -11161,14 +11159,9 @@ pub const Lowering = struct {
|
||||
}
|
||||
var ret_is_self = false;
|
||||
const ret = if (method.return_type) |rt| blk: {
|
||||
if (rt.data == .type_expr) {
|
||||
if (std.mem.eql(u8, rt.data.type_expr.name, "Self")) {
|
||||
ret_is_self = true;
|
||||
break :blk void_ptr_ty;
|
||||
}
|
||||
if (self.type_alias_map.get(rt.data.type_expr.name)) |aliased| {
|
||||
break :blk aliased;
|
||||
}
|
||||
if (rt.data == .type_expr and std.mem.eql(u8, rt.data.type_expr.name, "Self")) {
|
||||
ret_is_self = true;
|
||||
break :blk void_ptr_ty;
|
||||
}
|
||||
break :blk type_bridge.resolveAstType(rt, table);
|
||||
} else .void;
|
||||
|
||||
@@ -109,3 +109,49 @@ test "resolveAstType: null returns default" {
|
||||
|
||||
try std.testing.expectEqual(TypeId.s64, type_bridge.resolveAstType(null, &table));
|
||||
}
|
||||
|
||||
test "resolveAstType: TypeTable.aliases 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).
|
||||
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_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.
|
||||
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.
|
||||
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));
|
||||
|
||||
// Compound forms (`*Opaque`, `[]Opaque`, `?Opaque`) route
|
||||
// through recursive helpers that ultimately re-enter
|
||||
// `resolveTypeName` — the alias map is consulted 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);
|
||||
try std.testing.expectEqual(TypeInfo{ .pointer = .{ .pointee = .u64 } }, table.get(ptr_id));
|
||||
}
|
||||
|
||||
@@ -212,6 +212,18 @@ fn resolveTypeName(name: []const u8, table: *TypeTable) TypeId {
|
||||
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 = &.{} } });
|
||||
}
|
||||
|
||||
|
||||
@@ -247,6 +247,14 @@ pub const TypeTable = struct {
|
||||
alloc: Allocator,
|
||||
/// Target pointer size in bytes (4 for wasm32, 8 for 64-bit targets).
|
||||
pointer_size: u8 = 8,
|
||||
/// Borrowed pointer to Lowering's `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{
|
||||
|
||||
Reference in New Issue
Block a user