fix(ir): exhaustive named-const array dims (0083) + nested slice-literal coercion (0085)

Makes the F0.4 fixes exhaustive across every resolution / nesting path.

0083 — named-const array dimension, stateless paths. Attempt 1 fixed the
stateful resolver (direct local decls, struct fields, params, returns) but the
binding-free registration-time resolver (`type_bridge`, used for type aliases
`Arr :: [N]T` and inline union/enum field types) still resolved a named dim with
a silent `else 0`, so `Arr :: [N]s64; a : Arr` and `union { a: [N]s64 }` were
still miscompiled (garbage / bus error). Thread the module-global const table
(`ProgramIndex.module_const_map`) into `type_bridge` alongside the alias map, so
`StatelessInner.resolveArrayLen` resolves a named module-const dim to the same
length everywhere. The remaining unresolvable case (a computed/comptime dim on
the binding-free path, which the stateful path hard-errors) now bails LOUDLY
instead of fabricating a 0 length.

0085 — nested slice-literal elements. `lowerArrayLiteral` lowered each element
with the element type as target but appended the raw value. A nested `.[...]`
element at a slice element type (`[][]s64`) still lowers to an aggregate array
`[N]T`, so the outer aggregate held raw arrays where slice {ptr,len} headers
were expected — indexing the inner slice read a garbage pointer and segfaulted.
After lowering each element, coerce a same-element array to the slice element
type via the existing `array_to_slice` op. The coercion recurses with the
nesting, so `[][]T` and deeper materialize at every level — local-bound AND
direct-call-argument forms.

Regressions (fail-before/pass-after demonstrated on the pre-fix compiler):
  examples/0140-types-named-const-array-dim.sx — extended with type-alias,
    nested [N][M]T, and union-field named dims (s64 / string / struct elems)
  examples/0142-types-nested-slice-literal-elements.sx — [][]s64 + [][]string,
    local-bound vs direct-arg
  src/ir/type_bridge.test.zig — named-const dim resolves to literal length

Gate: zig build, zig build test, bash tests/run_examples.sh (388 passed).
Issues 0083 and 0085 marked RESOLVED.
This commit is contained in:
agra
2026-06-04 09:06:08 +03:00
parent 12552e125d
commit 1f9f944ca1
13 changed files with 329 additions and 92 deletions

View File

@@ -264,7 +264,7 @@ pub const CallResolver = struct {
if (method_fd.body.data == .compiler_expr) {
return .{
.kind = .struct_method,
.return_type = if (method_fd.return_type) |rt| type_bridge.resolveAstType(rt, &self.l.module.types, &self.l.program_index.type_alias_map) else .void,
.return_type = if (method_fd.return_type) |rt| type_bridge.resolveAstType(rt, &self.l.module.types, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map) else .void,
.target = .{ .named = qualified },
.prepends_receiver = true,
.expands_defaults = defaultsFor(method_fd, c.args.len + 1),

View File

@@ -560,9 +560,9 @@ pub const Lowering = struct {
} else if (cd.value.data == .struct_decl) {
self.registerStructDecl(&cd.value.data.struct_decl);
} else if (cd.value.data == .enum_decl) {
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map);
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
} else if (cd.value.data == .union_decl) {
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map);
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
} else if (cd.value.data == .comptime_expr) {
self.lowerComptimeGlobal(cd.name, cd.value.data.comptime_expr.expr, cd.type_annotation);
}
@@ -574,10 +574,10 @@ pub const Lowering = struct {
self.registerStructDecl(&sd);
},
.enum_decl => {
_ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map);
_ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
},
.union_decl => {
_ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map);
_ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
},
.error_set_decl => {
self.registerErrorSetDecl(decl);
@@ -675,10 +675,10 @@ pub const Lowering = struct {
self.registerStructDecl(&cd.value.data.struct_decl);
} else if (cd.value.data == .enum_decl) {
// Register enum/tagged-union types in the type table
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map);
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
} else if (cd.value.data == .union_decl) {
// Register plain union types in the type table
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map);
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
} else if (cd.value.data == .type_expr or
cd.value.data == .pointer_type_expr or
cd.value.data == .many_pointer_type_expr or
@@ -688,7 +688,7 @@ pub const Lowering = struct {
cd.value.data == .function_type_expr)
{
// Type alias: MyFloat :: f64; Ptr :: *u8; Cb :: (s32) -> s32;
const target_ty = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map);
const target_ty = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
self.program_index.type_alias_map.put(cd.name, target_ty) catch {};
} else if (cd.value.data == .identifier) {
// Identifier-RHS alias: MyAlias :: MyInt; WideAlias :: Wide;
@@ -771,7 +771,7 @@ pub const Lowering = struct {
// resolve via type_bridge and register the result
// under the alias name so `Vec4` in expression
// position can `const_type(<vector tid>)`.
const result_ty = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map);
const result_ty = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
if (result_ty != .void and result_ty != .unresolved) {
self.program_index.type_alias_map.put(cd.name, result_ty) catch {};
}
@@ -806,11 +806,11 @@ pub const Lowering = struct {
},
.enum_decl => {
// Register enum/tagged-union types in the type table
_ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map);
_ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
},
.union_decl => {
// Register plain union types in the type table
_ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map);
_ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
},
.error_set_decl => {
self.registerErrorSetDecl(decl);
@@ -1813,7 +1813,7 @@ pub const Lowering = struct {
// Block-local type declarations
.struct_decl => |sd| self.registerStructDecl(&sd),
.enum_decl, .union_decl => {
_ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map);
_ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
},
.error_set_decl => self.registerErrorSetDecl(node),
.ufcs_alias => |ua| {
@@ -1972,7 +1972,7 @@ pub const Lowering = struct {
return;
}
if (cd.value.data == .enum_decl or cd.value.data == .union_decl) {
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map);
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
return;
}
@@ -2912,7 +2912,7 @@ pub const Lowering = struct {
// `t : Type = f64;` store a real TypeId; lets
// `t == f64` icmp at runtime against the same TypeId.
if (self.isKnownTypeName(te.name)) {
const ty = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map);
const ty = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
break :blk self.builder.constType(ty);
}
break :blk self.emitError(te.name, node.span);
@@ -5357,8 +5357,26 @@ pub const Lowering = struct {
for (al.elements) |elem| {
const old_tt = self.target_type;
self.target_type = elem_ty;
const val = self.lowerExpr(elem);
var val = self.lowerExpr(elem);
self.target_type = old_tt;
// A nested `.[...]` element at a slice element type lowers to an
// aggregate array `[N]U` (lowerArrayLiteral always yields an array
// value); materialize it into a `[]U` slice so the element is a real
// {ptr,len} header rather than a raw array the callee would read its
// header off of (issue 0085). This per-element coercion recurses with
// the literal nesting, so `[][]T` and deeper coerce at every level.
if (!elem_ty.isBuiltin()) {
const ei = self.module.types.get(elem_ty);
if (ei == .slice) {
const val_ty = self.builder.getRefType(val);
if (!val_ty.isBuiltin()) {
const vi = self.module.types.get(val_ty);
if (vi == .array and vi.array.element == ei.slice.element) {
val = self.coerceToType(val, val_ty, elem_ty);
}
}
}
}
elems.append(self.alloc, val) catch unreachable;
}
@@ -5401,7 +5419,7 @@ pub const Lowering = struct {
const name_id = self.module.types.internString(id.name);
return self.module.types.findByName(name_id) orelse .unresolved;
},
.type_expr => return type_bridge.resolveAstType(te, &self.module.types, &self.program_index.type_alias_map),
.type_expr => return type_bridge.resolveAstType(te, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map),
.field_access => |fa| {
// Module.Type — try to resolve the field as a type name
const name_id = self.module.types.internString(fa.field);
@@ -6875,7 +6893,7 @@ pub const Lowering = struct {
// Check for #compiler free functions
if (self.program_index.fn_ast_map.get(func_name)) |fd_check| {
if (fd_check.body.data == .compiler_expr) {
const ret_ty = if (fd_check.return_type) |rt| type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map) else TypeId.void;
const ret_ty = if (fd_check.return_type) |rt| type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map) else TypeId.void;
return self.builder.compilerCall(func_name, args.items, ret_ty);
}
}
@@ -7230,7 +7248,7 @@ pub const Lowering = struct {
if (self.program_index.fn_ast_map.get(qualified)) |method_fd| {
if (method_fd.body.data == .compiler_expr) {
const ret_ty = if (method_fd.return_type) |rt|
type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map)
type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map)
else
.void;
return self.builder.compilerCall(qualified, method_args.items, ret_ty);
@@ -7668,7 +7686,7 @@ pub const Lowering = struct {
const ret_ty = blk: {
if (lam.return_type) |rt| {
break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map);
break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
}
// Use target closure return type if available — but only when it's
// a resolved type. An `.unresolved` ret comes from an unbound
@@ -8238,7 +8256,7 @@ pub const Lowering = struct {
}
fn resolveReturnType2(self: *Lowering, rt: ?*const Node) TypeId {
if (rt) |r| return type_bridge.resolveAstType(r, &self.module.types, &self.program_index.type_alias_map);
if (rt) |r| return type_bridge.resolveAstType(r, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
return .void;
}
@@ -9278,8 +9296,8 @@ pub const Lowering = struct {
const ret_ty: TypeId = blk: {
if (fd.return_type) |rt| {
if (rt.data == .type_expr) {
if (type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map) != .unresolved) {
break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map);
if (type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map) != .unresolved) {
break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
}
}
}
@@ -10551,7 +10569,7 @@ pub const Lowering = struct {
return .unresolved;
}
}
return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map);
return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
}
pub fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId {
@@ -10612,7 +10630,7 @@ pub const Lowering = struct {
},
.type_expr => |te| {
if (self.program_index.type_alias_map.get(te.name)) |alias_ty| return alias_ty;
return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map);
return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
},
.call => |cl| {
// `type_of(x)` resolves to `inferExprType(x)` at lower
@@ -10637,7 +10655,7 @@ pub const Lowering = struct {
.slice_type_expr,
.optional_type_expr,
.function_type_expr,
=> return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map),
=> return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map),
else => return .unresolved,
}
}
@@ -11764,7 +11782,7 @@ pub const Lowering = struct {
// literal (`(s32, s32)`); validate its elements are types and reject
// non-type elements loudly (issue 0067).
.tuple_literal => return self.resolveTupleLiteralTypeArg(node),
else => return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map),
else => return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map),
}
}
@@ -12195,7 +12213,7 @@ pub const Lowering = struct {
}
return;
}
_ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map);
_ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
}
fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl) void {
@@ -12341,7 +12359,7 @@ pub const Lowering = struct {
if (const_node.data == .const_decl) {
const cd = const_node.data.const_decl;
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, cd.name }) catch continue;
const ty: ?TypeId = if (cd.type_annotation) |ta| type_bridge.resolveAstType(ta, table, &self.program_index.type_alias_map) else null;
const ty: ?TypeId = if (cd.type_annotation) |ta| type_bridge.resolveAstType(ta, table, &self.program_index.type_alias_map, &self.program_index.module_const_map) else null;
self.struct_const_map.put(qualified, .{ .value = cd.value, .ty = ty }) catch {};
}
}

View File

@@ -296,7 +296,7 @@ pub const ProtocolResolver = struct {
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, &self.l.program_index.type_alias_map);
break :blk type_bridge.resolveAstType(p, table, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map);
};
ptypes.append(self.l.alloc, pty) catch unreachable;
}
@@ -306,7 +306,7 @@ pub const ProtocolResolver = struct {
ret_is_self = true;
break :blk void_ptr_ty;
}
break :blk type_bridge.resolveAstType(rt, table, &self.l.program_index.type_alias_map);
break :blk type_bridge.resolveAstType(rt, table, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map);
} else .void;
method_infos.append(self.l.alloc, .{
.name = method.name,
@@ -393,7 +393,7 @@ pub const ProtocolResolver = struct {
// Resolve the protocol's type-arg list to concrete TypeIds.
var arg_tys = std.ArrayList(TypeId).empty;
for (ib.protocol_type_args) |arg_node| {
const t = type_bridge.resolveAstType(arg_node, table, &self.l.program_index.type_alias_map);
const t = type_bridge.resolveAstType(arg_node, table, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map);
arg_tys.append(self.l.alloc, t) catch return;
}
@@ -401,9 +401,9 @@ pub const ProtocolResolver = struct {
// parameterised impls (back-compat `target_type` string is kept for
// simple cases but the canonical form is the TypeExpr).
const src_ty: TypeId = if (ib.target_type_expr) |te|
type_bridge.resolveAstType(te, table, &self.l.program_index.type_alias_map)
type_bridge.resolveAstType(te, table, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map)
else if (ib.target_type.len > 0)
type_bridge.resolveAstType(&.{ .span = decl.span, .data = .{ .type_expr = .{ .name = ib.target_type } } }, table, &self.l.program_index.type_alias_map)
type_bridge.resolveAstType(&.{ .span = decl.span, .data = .{ .type_expr = .{ .name = ib.target_type } } }, table, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map)
else
return;

View File

@@ -3,6 +3,8 @@ const std = @import("std");
const types = @import("types.zig");
const type_bridge = @import("type_bridge.zig");
const ast = @import("../ast.zig");
const program_index_mod = @import("program_index.zig");
const ModuleConstInfo = program_index_mod.ModuleConstInfo;
const Node = ast.Node;
const TypeId = types.TypeId;
@@ -18,7 +20,7 @@ test "resolveAstType: primitive type_expr" {
defer alloc.destroy(node);
node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "f64" } } };
try std.testing.expectEqual(TypeId.f64, type_bridge.resolveAstType(node, &table, null));
try std.testing.expectEqual(TypeId.f64, type_bridge.resolveAstType(node, &table, null, null));
}
test "resolveAstType: pointer type" {
@@ -34,7 +36,7 @@ test "resolveAstType: pointer type" {
defer alloc.destroy(node);
node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .pointer_type_expr = .{ .pointee_type = inner } } };
const id = type_bridge.resolveAstType(node, &table, null);
const id = type_bridge.resolveAstType(node, &table, null, null);
try std.testing.expectEqual(TypeInfo{ .pointer = .{ .pointee = .s32 } }, table.get(id));
}
@@ -55,7 +57,7 @@ test "resolveAstType: optional slice" {
defer alloc.destroy(opt);
opt.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .optional_type_expr = .{ .inner_type = slice } } };
const id = type_bridge.resolveAstType(opt, &table, null);
const id = type_bridge.resolveAstType(opt, &table, null, null);
const info = table.get(id);
switch (info) {
.optional => |o| {
@@ -71,7 +73,7 @@ test "resolveAstType: null surfaces as .unresolved (no silent s64 default)" {
var table = TypeTable.init(alloc);
defer table.deinit();
try std.testing.expectEqual(TypeId.unresolved, type_bridge.resolveAstType(null, &table, null));
try std.testing.expectEqual(TypeId.unresolved, type_bridge.resolveAstType(null, &table, null, null));
}
test "resolveAstType: threaded alias_map resolves named alias" {
@@ -85,7 +87,7 @@ test "resolveAstType: threaded alias_map resolves named alias" {
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, null);
const empty_stub = type_bridge.resolveAstType(sh_node, &table, null, null);
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);
@@ -102,7 +104,7 @@ test "resolveAstType: threaded alias_map resolves named alias" {
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, &aliases));
try std.testing.expectEqual(TypeId.u64, type_bridge.resolveAstType(opaque_node, &table, &aliases, null));
// Compound forms (`*Opaque`, `[]Opaque`, `?Opaque`) route through recursive
// helpers that thread the same alias_map at every step.
@@ -112,10 +114,42 @@ test "resolveAstType: threaded alias_map resolves named alias" {
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, &aliases);
const ptr_id = type_bridge.resolveAstType(ptr_node, &table, &aliases, null);
try std.testing.expectEqual(TypeInfo{ .pointer = .{ .pointee = .u64 } }, table.get(ptr_id));
}
test "resolveAstType: named-const array dimension resolves to the same length as a literal (issue 0083)" {
const alloc = std.testing.allocator;
var table = TypeTable.init(alloc);
defer table.deinit();
// `N :: 4` in the module-const table, value backed by an int-literal node.
const n_val = try alloc.create(Node);
defer alloc.destroy(n_val);
n_val.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .int_literal = .{ .value = 4 } } };
var consts = std.StringHashMap(ModuleConstInfo).init(alloc);
defer consts.deinit();
try consts.put("N", .{ .value = n_val, .ty = .s64 });
// `[N]s64` — dimension is the named const `N`, not a literal.
const elem = try alloc.create(Node);
defer alloc.destroy(elem);
elem.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "s64" } } };
const len_node = try alloc.create(Node);
defer alloc.destroy(len_node);
len_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .identifier = .{ .name = "N" } } };
const arr = try alloc.create(Node);
defer alloc.destroy(arr);
arr.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .array_type_expr = .{ .length = len_node, .element_type = elem } } };
// With the const table threaded, `[N]s64` lays out identically to `[4]s64`.
const id = type_bridge.resolveAstType(arr, &table, null, &consts);
const info = table.get(id);
try std.testing.expect(info == .array);
try std.testing.expectEqual(TypeId.s64, info.array.element);
try std.testing.expectEqual(@as(u32, 4), info.array.length);
}
test "resolveAstType: error_set_decl registers an error-set type + interns tags" {
const alloc = std.testing.allocator;
var table = TypeTable.init(alloc);
@@ -129,7 +163,7 @@ test "resolveAstType: error_set_decl registers an error-set type + interns tags"
.tag_names = &tag_names,
} } };
const id = type_bridge.resolveAstType(node, &table, null);
const id = type_bridge.resolveAstType(node, &table, null, null);
const info = table.get(id);
try std.testing.expect(info == .error_set);
try std.testing.expectEqualStrings("ParseErr", table.getString(info.error_set.name));
@@ -137,7 +171,7 @@ test "resolveAstType: error_set_decl registers an error-set type + interns tags"
// Tags were interned into the global pool (round-trip a name through it).
try std.testing.expectEqualStrings("BadDigit", table.getTagName(table.internTag("BadDigit")));
// Re-resolving the same decl dedups to the same TypeId.
try std.testing.expectEqual(id, type_bridge.resolveAstType(node, &table, null));
try std.testing.expectEqual(id, type_bridge.resolveAstType(node, &table, null, null));
}
// ── ERR E1.2 — failable-signature error channel resolution ──
@@ -154,7 +188,7 @@ test "resolveAstType: `!Named` resolves to the declared error set" {
const node = try alloc.create(Node);
defer alloc.destroy(node);
node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .error_type_expr = .{ .name = "ParseErr" } } };
try std.testing.expectEqual(set, type_bridge.resolveAstType(node, &table, null));
try std.testing.expectEqual(set, type_bridge.resolveAstType(node, &table, null, null));
}
test "resolveAstType: bare `!` resolves to a shared inferred placeholder set" {
@@ -169,8 +203,8 @@ test "resolveAstType: bare `!` resolves to a shared inferred placeholder set" {
defer alloc.destroy(b);
b.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .error_type_expr = .{ .name = null } } };
const ia = type_bridge.resolveAstType(a, &table, null);
const ib = type_bridge.resolveAstType(b, &table, null);
const ia = type_bridge.resolveAstType(a, &table, null, null);
const ib = type_bridge.resolveAstType(b, &table, null, null);
try std.testing.expect(table.get(ia) == .error_set);
try std.testing.expectEqualStrings("!", table.getString(table.get(ia).error_set.name));
try std.testing.expectEqual(@as(usize, 0), table.get(ia).error_set.tags.len); // empty until E1.4 SCC
@@ -198,7 +232,7 @@ test "resolveAstType: `(s32, !Named)` result list is a tuple ending in the error
defer alloc.destroy(tuple);
tuple.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .tuple_type_expr = .{ .field_types = &fields, .field_names = null } } };
const id = type_bridge.resolveAstType(tuple, &table, null);
const id = type_bridge.resolveAstType(tuple, &table, null, null);
const info = table.get(id);
try std.testing.expect(info == .tuple);
try std.testing.expectEqual(@as(usize, 2), info.tuple.fields.len);

View File

@@ -8,6 +8,8 @@ const TypeInfo = ir_types.TypeInfo;
const TypeTable = ir_types.TypeTable;
const StringId = ir_types.StringId;
const type_resolver = @import("type_resolver.zig");
const program_index_mod = @import("program_index.zig");
const ModuleConstInfo = program_index_mod.ModuleConstInfo;
/// The single-source type-alias table (`ProgramIndex.type_alias_map`), threaded
/// explicitly through every name-resolving entry point so a bare name like
@@ -17,6 +19,15 @@ const type_resolver = @import("type_resolver.zig");
/// `null` for contexts that never see aliases, e.g. unit tests).
pub const AliasMap = ?*const std.StringHashMap(TypeId);
/// The module-global constant table (`ProgramIndex.module_const_map`), threaded
/// alongside the alias map so a named-const array dimension (`N :: 16; [N]T`)
/// resolves to the same length as a literal dimension on EVERY registration-time
/// path — type aliases (`Arr :: [N]T`), inline union/enum field types — not just
/// the stateful body-lowering path. Without it the stateless dim resolver had no
/// way to evaluate a named const and silently fabricated a 0 length (issue 0083).
/// `null` for contexts with no const table (e.g. unit tests).
pub const ConstMap = ?*const std.StringHashMap(ModuleConstInfo);
/// Binding-free element-recursion adapter for `TypeResolver.resolveCompound`:
/// nested element types resolve through `type_bridge.resolveAstType` (the
/// registration-time path — no generic/pack bindings). Lets type_bridge reuse
@@ -25,20 +36,35 @@ pub const AliasMap = ?*const std.StringHashMap(TypeId);
const StatelessInner = struct {
table: *TypeTable,
alias_map: AliasMap,
consts: ConstMap,
pub fn resolveInner(self: StatelessInner, node: *const Node) TypeId {
return resolveAstType(node, self.table, self.alias_map);
return resolveAstType(node, self.table, self.alias_map, self.consts);
}
/// Fixed-array dimension at registration time (no bindings / const tables).
/// Only a literal dimension is knowable here; a named-const dimension
/// (`N :: 16; [N]T`) is resolved by the stateful caller
/// (`Lowering.resolveArrayLen`) before it ever reaches this binding-free
/// path — mirroring how `pack_index_type_expr` is handled stateful-first.
/// Fixed-array dimension at registration time: a literal `[16]T`, or a
/// named module-global const `N :: 16; [N]T` looked up in the const table.
/// Both yield the SAME length — registration-time paths (aliases, inline
/// union/enum fields) must lay out a named-const dim identically to a literal
/// (issue 0083). A dimension that is neither is not resolvable on this
/// binding-free path (it would be a computed/comptime expression, which the
/// stateful body-lowering path diagnoses as a hard error at the storage
/// site); bail LOUDLY rather than fabricating a 0 length that silently gives a
/// 0-byte array and out-of-bounds element access.
pub fn resolveArrayLen(self: StatelessInner, len_node: *const Node) u32 {
_ = self;
return switch (len_node.data) {
.int_literal => |lit| @intCast(lit.value),
else => 0,
};
switch (len_node.data) {
.int_literal => |lit| return @intCast(lit.value),
.identifier => |id| if (self.namedConstLen(id.name)) |n| return n,
.type_expr => |te| if (self.namedConstLen(te.name)) |n| return n,
else => {},
}
std.debug.print("type_bridge: array dimension is not a literal or named integer constant — cannot resolve length at registration time (computed/comptime dimensions are unsupported here)\n", .{});
return 0;
}
/// A name that resolves to a module-global integer constant → its value.
fn namedConstLen(self: StatelessInner, name: []const u8) ?u32 {
const consts = self.consts orelse return null;
const ci = consts.get(name) orelse return null;
if (ci.value.data == .int_literal) return @intCast(ci.value.data.int_literal.value);
return null;
}
};
@@ -46,14 +72,14 @@ const StatelessInner = struct {
// Resolve an AST type node into an IR TypeId. Used during lowering when
// we only have the parsed AST (no codegen type registry).
pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap) TypeId {
pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId {
// A null node means a caller reached type resolution without a type node.
// Every current caller either passes a non-optional node or handles the
// "no type" case itself (returning `.void`), so this is a caller bug — and
// `.s64` here would silently fabricate an 8-byte int. Surface it via the
// `.unresolved` sentinel (trips the sizeOf/toLLVMType panic at codegen).
const n = node orelse return .unresolved;
const si = StatelessInner{ .table = table, .alias_map = alias_map };
const si = StatelessInner{ .table = table, .alias_map = alias_map, .consts = consts };
return switch (n.data) {
.type_expr => |te| resolveTypeName(te.name, table, alias_map),
.identifier => |id| resolveTypeName(id.name, table, alias_map),
@@ -76,8 +102,8 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap
// `Closure(..p)` field type at registration time). These tiny fallbacks
// are the only stateless-specific shape code left; the stateful expand
// lives in PackResolver.
.closure_type_expr => |ct| type_resolver.TypeResolver.resolveCompound(table, n, si) orelse resolveClosurePackShape(&ct, table, alias_map),
.tuple_type_expr => |tt| type_resolver.TypeResolver.resolveCompound(table, n, si) orelse resolveTupleSpreadShape(&tt, table, alias_map),
.closure_type_expr => |ct| type_resolver.TypeResolver.resolveCompound(table, n, si) orelse resolveClosurePackShape(&ct, table, alias_map, consts),
.tuple_type_expr => |tt| type_resolver.TypeResolver.resolveCompound(table, n, si) orelse resolveTupleSpreadShape(&tt, table, alias_map, consts),
.pack_index_type_expr => {
// Pack-index `$args[N]` in a type position must be resolved
// against an active pack binding — `type_bridge` has no access
@@ -90,8 +116,8 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap
std.debug.print("type_bridge: pack-index type expression encountered outside a pack-aware context — returning .unresolved\n", .{});
return .unresolved;
},
.tuple_literal => |tl| resolveTupleLiteralAsType(&tl, table, alias_map),
.parameterized_type_expr => |pt| resolveParameterizedType(&pt, table, alias_map),
.tuple_literal => |tl| resolveTupleLiteralAsType(&tl, table, alias_map, consts),
.parameterized_type_expr => |pt| resolveParameterizedType(&pt, table, alias_map, consts),
// An unannotated param. Its type must be resolved from context
// (contextual closure typing, generic binding, or pack substitution)
// *before* reaching here; if it doesn't, returning a plausible `.s64`
@@ -101,9 +127,9 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap
// turns it into a real diagnostic.
.inferred_type => .unresolved,
// Inline type declarations (used as field types)
.enum_decl => |ed| resolveInlineEnum(&ed, table, alias_map),
.struct_decl => |sd| resolveInlineStruct(&sd, table, alias_map),
.union_decl => |ud| resolveInlineUnion(&ud, table, alias_map),
.enum_decl => |ed| resolveInlineEnum(&ed, table, alias_map, consts),
.struct_decl => |sd| resolveInlineStruct(&sd, table, alias_map, consts),
.union_decl => |ud| resolveInlineUnion(&ud, table, alias_map, consts),
.error_set_decl => |esd| resolveInlineErrorSet(&esd, table),
.error_type_expr => |ete| resolveErrorType(&ete, table, alias_map),
else => {
@@ -137,13 +163,13 @@ pub const resolveTypePrimitive = type_resolver.TypeResolver.resolvePrimitive;
/// null). type_bridge can't expand the pack (no state), so it preserves the
/// pack SHAPE — a `closureTypePack` whose prefix is the fixed params. The
/// stateful expand lives in `PackResolver.resolveClosureTypeWithBindings`.
fn resolveClosurePackShape(ct: *const ast.ClosureTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId {
fn resolveClosurePackShape(ct: *const ast.ClosureTypeExpr, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId {
const alloc = table.alloc;
var param_ids = std.ArrayList(TypeId).empty;
for (ct.param_types) |pt| {
param_ids.append(alloc, resolveAstType(pt, table, alias_map)) catch unreachable;
param_ids.append(alloc, resolveAstType(pt, table, alias_map, consts)) catch unreachable;
}
const ret_id = if (ct.return_type) |rt| resolveAstType(rt, table, alias_map) else TypeId.void;
const ret_id = if (ct.return_type) |rt| resolveAstType(rt, table, alias_map, consts) else TypeId.void;
return table.closureTypePack(param_ids.items, ret_id, @intCast(param_ids.items.len));
}
@@ -152,11 +178,11 @@ fn resolveClosurePackShape(ct: *const ast.ClosureTypeExpr, table: *TypeTable, al
/// each field resolves individually (a spread field is not a type → resolves to
/// `.unresolved`). The stateful expand lives in
/// `PackResolver.resolveTupleTypeWithBindings`.
fn resolveTupleSpreadShape(tt: *const ast.TupleTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId {
fn resolveTupleSpreadShape(tt: *const ast.TupleTypeExpr, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId {
const alloc = table.alloc;
var field_ids = std.ArrayList(TypeId).empty;
for (tt.field_types) |ft| {
field_ids.append(alloc, resolveAstType(ft, table, alias_map)) catch unreachable;
field_ids.append(alloc, resolveAstType(ft, table, alias_map, consts)) catch unreachable;
}
var name_ids: ?[]const StringId = null;
if (tt.field_names) |names| {
@@ -182,14 +208,14 @@ fn resolveTupleSpreadShape(tt: *const ast.TupleTypeExpr, table: *TypeTable, alia
// here, so the valid path below builds the tuple and the invalid path never
// reaches it from lowering. The sentinel is the backstop for any other
// (binding-free) caller.
fn resolveTupleLiteralAsType(tl: *const ast.TupleLiteral, table: *TypeTable, alias_map: AliasMap) TypeId {
fn resolveTupleLiteralAsType(tl: *const ast.TupleLiteral, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId {
const alloc = table.alloc;
var field_ids = std.ArrayList(TypeId).empty;
var name_ids_list = std.ArrayList(StringId).empty;
var any_named = false;
for (tl.elements) |el| {
if (!isTypeShapedAstNode(el.value, table)) return .unresolved;
field_ids.append(alloc, resolveAstType(el.value, table, alias_map)) catch unreachable;
field_ids.append(alloc, resolveAstType(el.value, table, alias_map, consts)) catch unreachable;
if (el.name) |n| {
any_named = true;
name_ids_list.append(alloc, table.internString(n)) catch unreachable;
@@ -233,7 +259,7 @@ pub fn isTypeShapedAstNode(node: *const Node, table: *TypeTable) bool {
};
}
fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId {
fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId {
// Strip module prefix (e.g. "std.Vector" → "Vector")
const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name;
// Vector(N, T) is a built-in parameterized type
@@ -243,7 +269,7 @@ fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTa
.int_literal => |lit| @intCast(@as(u64, @bitCast(lit.value))),
else => 0,
};
const elem = resolveAstType(pt.args[1], table, alias_map);
const elem = resolveAstType(pt.args[1], table, alias_map, consts);
return table.vectorOf(elem, length);
}
}
@@ -254,7 +280,7 @@ fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTa
// ── Inline type declarations ─────────────────────────────────────────
fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: AliasMap) TypeId {
fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId {
const alloc = table.alloc;
const name_id = table.internString(ed.name);
@@ -280,7 +306,7 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia
} else {
var sfields = std.ArrayList(TypeInfo.StructInfo.Field).empty;
for (sd.field_names, sd.field_types) |fname, ftype_node| {
const fty = resolveAstType(ftype_node, table, alias_map);
const fty = resolveAstType(ftype_node, table, alias_map, consts);
sfields.append(alloc, .{
.name = table.internString(fname),
.ty = fty,
@@ -294,10 +320,10 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia
table.update(field_ty, sinfo);
}
} else {
field_ty = resolveAstType(vt, table, alias_map);
field_ty = resolveAstType(vt, table, alias_map, consts);
}
} else {
field_ty = resolveAstType(vt, table, alias_map);
field_ty = resolveAstType(vt, table, alias_map, consts);
}
}
}
@@ -311,7 +337,7 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia
var backing_type: ?TypeId = null;
var tag_type: ?TypeId = null;
if (ed.backing_type) |bt| {
const backing_ty = resolveAstType(bt, table, alias_map);
const backing_ty = resolveAstType(bt, table, alias_map, consts);
backing_type = backing_ty;
// Extract tag type from first field of backing struct
const backing_info = table.get(backing_ty);
@@ -394,7 +420,7 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia
if (ed.backing_type) |bt| {
// Only use simple backing types (u8, u16, u32, etc.), not struct backing (enum struct)
if (bt.data != .struct_decl) {
enum_backing = resolveAstType(bt, table, alias_map);
enum_backing = resolveAstType(bt, table, alias_map, consts);
}
}
@@ -410,7 +436,7 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia
return id;
}
fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable, alias_map: AliasMap) TypeId {
fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId {
const alloc = table.alloc;
const name_id = table.internString(sd.name);
@@ -418,7 +444,7 @@ fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable, alias_map:
var fields = std.ArrayList(TypeInfo.StructInfo.Field).empty;
for (sd.field_names, sd.field_types) |fname, ftype_node| {
const field_ty = resolveAstType(ftype_node, table, alias_map);
const field_ty = resolveAstType(ftype_node, table, alias_map, consts);
fields.append(alloc, .{
.name = table.internString(fname),
.ty = field_ty,
@@ -433,7 +459,7 @@ fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable, alias_map:
return id;
}
fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable, alias_map: AliasMap) TypeId {
fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId {
const alloc = table.alloc;
const name_id = table.internString(ud.name);
@@ -441,7 +467,7 @@ fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable, alias_map: Al
var fields = std.ArrayList(TypeInfo.StructInfo.Field).empty;
for (ud.field_names, ud.field_types) |fname, ftype_node| {
const field_ty = resolveAstType(ftype_node, table, alias_map);
const field_ty = resolveAstType(ftype_node, table, alias_map, consts);
fields.append(alloc, .{
.name = table.internString(fname),
.ty = field_ty,