fix(ir): unify named-const array-dim resolution + kill length-0 fabrication (0083)
A type alias whose dimension is a named const (`Arr :: [N]T`) resolves its dimension eagerly during scanDecls pass 1, on the stateless registration path, which can only read `module_const_map`. Typed consts (`N : s64 : 16`) register only in pass 2 and a forward-declared untyped const had not registered yet, so the stateless resolver saw an empty table, printed a non-fatal warning, fabricated length 0, and continued — yielding a 0-byte alloca, garbage reads, and a segfault for slice/struct elements. - scanDecls pass 0 pre-registers every integer-valued module const before any type alias resolves, so typed, untyped, and forward-referenced consts all resolve identically. - Both dim resolvers now share `program_index.moduleConstInt`, so the stateful body-lowering path and the stateless registration path cannot diverge. - `resolveArrayLen` returns `?u32`; `resolveCompound` yields `.unresolved` on null instead of a 0-length array. The stateful path emits a diagnostic; the alias-registration path surfaces an unresolved alias as a clean compile error that aborts the build. The Vector lane-count `else => 0` is fixed the same way. Regressions: examples/0143 (typed-const dim direct + via alias for s64/string/ struct, forward-ref alias, nested) and examples/1129 (an unresolvable computed dim halts with a clean diagnostic + non-zero exit). Both fail on the pre-fix compiler (garbage/segfault; warning+exit0) and pass after.
This commit is contained in:
@@ -653,6 +653,24 @@ pub const Lowering = struct {
|
||||
|
||||
/// Pass 1: Scan declarations — register ASTs and extern stubs, but don't lower bodies.
|
||||
fn scanDecls(self: *Lowering, decls: []const *const Node) void {
|
||||
// Pass 0: register every integer-valued module const (`N :: 16` and the
|
||||
// typed `N : s64 : 16`) BEFORE any type alias is resolved below. A type
|
||||
// alias whose dimension is a named const (`Arr :: [N]T`) resolves its
|
||||
// dimension eagerly here, on the stateless registration path; that path
|
||||
// can only read `module_const_map`. Untyped consts would otherwise be
|
||||
// registered only in declaration order (pass 1) and typed ones only after
|
||||
// the alias fixpoint (pass 2) — so an alias declared before its const, or
|
||||
// any alias over a typed const, saw an empty table and miscompiled the
|
||||
// dimension to length 0 (issue 0083). The dimension only needs the value,
|
||||
// so a placeholder type is fine; pass 2 overwrites typed consts with the
|
||||
// resolved annotation type (issue 0070).
|
||||
for (decls) |decl| {
|
||||
if (decl.data != .const_decl) continue;
|
||||
const cd = decl.data.const_decl;
|
||||
if (cd.value.data == .int_literal) {
|
||||
self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .s64 }) catch {};
|
||||
}
|
||||
}
|
||||
for (decls) |decl| {
|
||||
self.setCurrentSourceFile(decl.source_file);
|
||||
const is_imported = if (self.main_file) |mf|
|
||||
@@ -689,6 +707,16 @@ pub const Lowering = struct {
|
||||
{
|
||||
// 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, &self.program_index.module_const_map);
|
||||
// The stateless resolver yields `.unresolved` for a shape
|
||||
// it cannot build — e.g. `Arr :: [<computed>]T`, whose
|
||||
// dimension is not a compile-time integer constant. Surface
|
||||
// it as a clean diagnostic so the build aborts here rather
|
||||
// than letting `.unresolved` reach codegen and `@panic` in
|
||||
// sizeOf (issue 0083 — no fabricated 0-length array).
|
||||
if (target_ty == .unresolved) {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, cd.value.span, "type alias '{s}' could not be resolved: an array dimension is not a compile-time integer constant", .{cd.name});
|
||||
}
|
||||
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;
|
||||
@@ -11634,10 +11662,12 @@ pub const Lowering = struct {
|
||||
/// `[16]T` and a named-const `N :: 16; [N]T` must resolve to the SAME length:
|
||||
/// the dimension is a compile-time integer, looked up in the comptime / value
|
||||
/// / module-const tables the stateful lowering owns. A dimension that isn't a
|
||||
/// compile-time integer is a hard error — emitting a diagnostic (rather than
|
||||
/// fabricating a 0 length, which gives a 0-byte array and out-of-bounds
|
||||
/// element access, issue 0083).
|
||||
pub fn resolveArrayLen(self: *Lowering, len_node: *const Node) u32 {
|
||||
/// compile-time integer is a hard error: emit a diagnostic so the driver
|
||||
/// aborts (`hasErrors()`), then return a harmless `0` so body lowering
|
||||
/// finishes without touching the `.unresolved` sentinel (which would `@panic`
|
||||
/// in `sizeOf` mid-lowering, before the diagnostic surfaces). The diagnostic —
|
||||
/// not the returned length — is what guarantees no garbage ships (issue 0083).
|
||||
pub fn resolveArrayLen(self: *Lowering, len_node: *const Node) ?u32 {
|
||||
if (self.comptimeArrayDim(len_node)) |n| {
|
||||
if (n < 0) {
|
||||
if (self.diagnostics) |d|
|
||||
@@ -11673,10 +11703,10 @@ pub const Lowering = struct {
|
||||
if (self.comptime_value_bindings) |cvb| {
|
||||
if (cvb.get(name)) |v| return v;
|
||||
}
|
||||
if (self.program_index.module_const_map.get(name)) |ci| {
|
||||
if (ci.value.data == .int_literal) return ci.value.data.int_literal.value;
|
||||
}
|
||||
return null;
|
||||
// The module-const branch is shared verbatim with the stateless
|
||||
// registration-time resolver (`type_bridge`) so a `[N]T` dimension
|
||||
// resolves to the same length on both paths (issue 0083).
|
||||
return program_index_mod.moduleConstInt(&self.program_index.module_const_map, name);
|
||||
}
|
||||
|
||||
/// Resolve a type node, checking type_bindings first for generic type params.
|
||||
|
||||
@@ -40,6 +40,21 @@ pub const ModuleConstInfo = struct {
|
||||
ty: TypeId,
|
||||
};
|
||||
|
||||
/// A name bound to a module-global integer constant → its value, else null.
|
||||
/// SINGLE source for both array-dimension resolvers — the stateful
|
||||
/// body-lowering path (`Lowering.comptimeIntNamed`) and the stateless
|
||||
/// registration-time path (`type_bridge.StatelessInner`). They must agree on
|
||||
/// which named consts a `[N]T` dimension resolves to; if they diverge, an array
|
||||
/// laid out via a type alias (`Arr :: [N]T`, stateless) gets a different length
|
||||
/// than the direct form (`a : [N]T`, stateful) — the issue-0083 miscompile.
|
||||
/// Untyped (`N :: 16`) and typed (`N : s64 : 16`) consts both store an
|
||||
/// `.int_literal` value node, so both resolve here identically.
|
||||
pub fn moduleConstInt(consts: *const std.StringHashMap(ModuleConstInfo), name: []const u8) ?i64 {
|
||||
const ci = consts.get(name) orelse return null;
|
||||
if (ci.value.data == .int_literal) return ci.value.data.int_literal.value;
|
||||
return null;
|
||||
}
|
||||
|
||||
pub const GlobalInfo = struct { id: inst.GlobalId, ty: TypeId };
|
||||
|
||||
/// Single lowering access point for declaration-name / import / visibility
|
||||
|
||||
@@ -41,30 +41,32 @@ const StatelessInner = struct {
|
||||
return resolveAstType(node, self.table, self.alias_map, self.consts);
|
||||
}
|
||||
/// 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 {
|
||||
/// named module-global const `N :: 16; [N]T` (typed `N : s64 : 16` too)
|
||||
/// 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). Returns null when the dimension
|
||||
/// is neither (a computed/comptime expression, or a name not bound to an
|
||||
/// integer const). Null propagates to `resolveCompound`, which yields the
|
||||
/// `.unresolved` sentinel rather than fabricating a 0 length that silently
|
||||
/// gives a 0-byte array and out-of-bounds element access; the registration
|
||||
/// caller surfaces the unresolved alias/type as a clean diagnostic.
|
||||
pub fn resolveArrayLen(self: StatelessInner, len_node: *const Node) ?u32 {
|
||||
switch (len_node.data) {
|
||||
.int_literal => |lit| return @intCast(lit.value),
|
||||
.int_literal => |lit| return if (lit.value >= 0) @intCast(lit.value) else null,
|
||||
.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;
|
||||
return null;
|
||||
}
|
||||
/// A name that resolves to a module-global integer constant → its value.
|
||||
/// A name that resolves to a non-negative module-global integer constant →
|
||||
/// its value. Shares `program_index.moduleConstInt` with the stateful
|
||||
/// body-lowering resolver so the two paths cannot disagree on which named
|
||||
/// consts a dimension resolves to (issue 0083).
|
||||
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;
|
||||
const v = program_index_mod.moduleConstInt(consts, name) orelse return null;
|
||||
return if (v >= 0) @intCast(v) else null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -265,10 +267,12 @@ fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTa
|
||||
// Vector(N, T) is a built-in parameterized type
|
||||
if (std.mem.eql(u8, base_name, "Vector")) {
|
||||
if (pt.args.len == 2) {
|
||||
const length: u32 = switch (pt.args[0].data) {
|
||||
.int_literal => |lit| @intCast(@as(u64, @bitCast(lit.value))),
|
||||
else => 0,
|
||||
};
|
||||
// The lane count is a literal or a named module-const integer — the
|
||||
// same dimension forms a fixed array accepts. An unresolvable count
|
||||
// is NOT a 0-lane vector (which would silently mis-size every load /
|
||||
// store); yield `.unresolved` so the failure surfaces (issue 0083).
|
||||
const si = StatelessInner{ .table = table, .alias_map = alias_map, .consts = consts };
|
||||
const length = si.resolveArrayLen(pt.args[0]) orelse return .unresolved;
|
||||
const elem = resolveAstType(pt.args[1], table, alias_map, consts);
|
||||
return table.vectorOf(elem, length);
|
||||
}
|
||||
|
||||
@@ -23,10 +23,10 @@ const PrimInner = struct {
|
||||
else => .unresolved,
|
||||
};
|
||||
}
|
||||
pub fn resolveArrayLen(_: PrimInner, len_node: *const Node) u32 {
|
||||
pub fn resolveArrayLen(_: PrimInner, len_node: *const Node) ?u32 {
|
||||
return switch (len_node.data) {
|
||||
.int_literal => |lit| @intCast(lit.value),
|
||||
else => 0,
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -95,10 +95,14 @@ pub const TypeResolver = struct {
|
||||
const elem = inner.resolveInner(at.element_type);
|
||||
// The dimension is delegated to `inner` exactly like the element
|
||||
// type: a literal `[16]T` and a named-const `N :: 16; [N]T` must
|
||||
// produce the same length. The stateful resolver consults the
|
||||
// const tables; the binding-free one handles literal dims (issue
|
||||
// 0083 — a 0 here gives a 0-byte array and OOB element access).
|
||||
const len = inner.resolveArrayLen(at.length);
|
||||
// produce the same length. `resolveArrayLen` returns null when the
|
||||
// dimension can't be resolved to a compile-time integer; that is
|
||||
// never a 0-length array (which gives a 0-byte alloca and OOB
|
||||
// element access — issue 0083). Yield the `.unresolved` sentinel
|
||||
// instead, so the failure halts the build (the stateful resolver
|
||||
// also emits a diagnostic; the registration-time caller surfaces
|
||||
// the unresolved alias) rather than silently miscompiling.
|
||||
const len = inner.resolveArrayLen(at.length) orelse break :blk TypeId.unresolved;
|
||||
break :blk table.arrayOf(elem, len);
|
||||
},
|
||||
.function_type_expr => |ft| blk: {
|
||||
|
||||
Reference in New Issue
Block a user