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:
agra
2026-05-28 15:12:53 +03:00
parent 6d258ad82b
commit 29bd182f3f
4 changed files with 83 additions and 24 deletions

View File

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

View File

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

View File

@@ -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 = &.{} } });
}

View File

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