From 29bd182f3f3b6468398b07316976111d3c74a54b Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 28 May 2026 15:12:53 +0300 Subject: [PATCH] ir: generalize type-alias resolution via TypeTable.aliases borrow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/ir/lower.zig | 41 ++++++++++++++------------------- src/ir/type_bridge.test.zig | 46 +++++++++++++++++++++++++++++++++++++ src/ir/type_bridge.zig | 12 ++++++++++ src/ir/types.zig | 8 +++++++ 4 files changed, 83 insertions(+), 24 deletions(-) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 2faf172..13ce9f6 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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; diff --git a/src/ir/type_bridge.test.zig b/src/ir/type_bridge.test.zig index 3ceb260..807adba 100644 --- a/src/ir/type_bridge.test.zig +++ b/src/ir/type_bridge.test.zig @@ -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)); +} diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index f943db4..da13a03 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -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 = &.{} } }); } diff --git a/src/ir/types.zig b/src/ir/types.zig index f8d2bec..5f93ff3 100644 --- a/src/ir/types.zig +++ b/src/ir/types.zig @@ -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{