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:
agra
2026-06-04 09:39:18 +03:00
parent 1f9f944ca1
commit d2bf8f3f2d
14 changed files with 211 additions and 34 deletions

View File

@@ -0,0 +1,65 @@
// A named-const array dimension lays out identically whether the const is
// TYPED (`N : s64 : 16`) or untyped (`N :: 16`), used DIRECTLY (`a : [N]T`) or
// through a type alias (`Arr :: [N]T`), and regardless of whether the const is
// declared before or after the alias that consumes it.
//
// Regression (issue 0083): the stateless registration-time resolver
// (type_bridge) only saw module consts that were already in `module_const_map`
// when a type alias resolved its dimension. Typed consts register in a later
// pass, and a forward-declared untyped const had not registered yet — so the
// alias dimension fabricated length 0 (a 0-byte alloca), and element access
// returned garbage (scalars) or bus-errored (slice/struct elements). Module
// consts are now pre-registered before any alias resolves, and both the
// stateful and stateless paths share one dimension resolver.
#import "modules/std.sx";
NT : s64 : 8; // typed const used as a dimension
P :: struct { x: s64; y: s64; }
// Type aliases whose dimension is the TYPED const NT (stateless registration).
TArr :: [NT]s64;
TSArr :: [NT]string;
TPArr :: [NT]P;
// Forward reference: this alias is declared BEFORE its dimension const NF.
FArr :: [NF]s64;
NF :: 5;
main :: () {
// Typed-const dimension, DIRECT local decl.
d : [NT]s64 = ---;
d[0] = 3;
d[7] = 21;
print("direct d0={} d7={} len={}\n", d[0], d[7], d.len);
// Typed-const dimension via ALIAS (scalar): same layout as the direct form.
a : TArr = ---;
a[0] = 7;
a[7] = 99;
print("alias a0={} a7={} len={}\n", a[0], a[7], a.len);
// Typed-const dimension via ALIAS (string elements): no bus error.
s : TSArr = ---;
s[0] = "hi";
s[7] = "yo";
print("alias s0={} s7={}\n", s[0], s[7]);
// Typed-const dimension via ALIAS (struct elements).
ps : TPArr = ---;
ps[0] = P.{ x = 1, y = 2 };
ps[7] = P.{ x = 5, y = 6 };
print("alias p0x={} p0y={} p7x={}\n", ps[0].x, ps[0].y, ps[7].x);
// Nested fixed array whose both dimensions are the typed const NT.
grid : [NT][NT]s64 = ---;
grid[0][0] = 1;
grid[7][7] = 10;
print("nested g00={} g77={}\n", grid[0][0], grid[7][7]);
// Forward-referenced alias dimension (untyped const declared after it).
f : FArr = ---;
f[0] = 4;
f[4] = 40;
print("fwd f0={} f4={} len={}\n", f[0], f[4], f.len);
}

View File

@@ -0,0 +1,20 @@
// An array dimension that is not a compile-time integer constant is a hard
// error, not a silently-fabricated 0-length array. Here a type alias's
// dimension is a computed expression (`M + 1`), which the registration-time
// resolver cannot evaluate.
//
// Regression (issue 0083): the stateless resolver printed a non-fatal warning
// and fabricated length 0, then let compilation continue — producing a 0-byte
// alloca and corrupt element access. It now yields the `.unresolved` sentinel,
// which the alias registration surfaces as this diagnostic, aborting the build
// with a non-zero exit.
#import "modules/std.sx";
M :: 4;
BadArr :: [M + 1]s64;
main :: () {
a : BadArr = ---;
a[0] = 7;
print("a0={}\n", a[0]);
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,6 @@
direct d0=3 d7=21 len=8
alias a0=7 a7=99 len=8
alias s0=hi s7=yo
alias p0x=1 p0y=2 p7x=5
nested g00=1 g77=10
fwd f0=4 f4=40 len=5

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: type alias 'BadArr' could not be resolved: an array dimension is not a compile-time integer constant
--> examples/1129-diagnostics-array-dim-not-const.sx:14:11
|
14 | BadArr :: [M + 1]s64;
| ^^^^^^^^^^

View File

@@ -0,0 +1 @@

View File

@@ -25,6 +25,30 @@
> `src/ir/type_bridge.zig`. Regression: `examples/0140-types-named-const-array-dim.sx` > `src/ir/type_bridge.zig`. Regression: `examples/0140-types-named-const-array-dim.sx`
> (direct + type-alias + nested `[N][M]T` + union-field dims, s64 / string / > (direct + type-alias + nested `[N][M]T` + union-field dims, s64 / string /
> struct element types). > struct element types).
>
> **Root-cause close-out (attempt 3).** Attempt 2 threaded the const map into
> `type_bridge` but the map wasn't fully populated when an alias resolved its
> dimension: type aliases (`Arr :: [N]T`) resolve EAGERLY in scanDecls pass 1,
> while TYPED consts (`N : s64 : 16`) register only in pass 2 and a
> forward-declared untyped const (`Arr :: [N]T; N :: 16`) hadn't registered yet
> either — so the stateless resolver saw an empty table, printed a non-fatal
> warning, fabricated length 0, and CONTINUED to garbage / a segfault. Three
> coordinated fixes: (1) a scanDecls **pass 0** pre-registers every integer-valued
> module const into `module_const_map` BEFORE any alias resolves, so typed,
> untyped, and forward-referenced consts all resolve identically; (2) both the
> stateful and stateless dim resolvers now share one routine
> (`program_index.moduleConstInt`) so they cannot disagree again; (3) the length-0
> fabrications are GONE — `resolveArrayLen` returns `?u32`, `resolveCompound`
> yields the `.unresolved` sentinel on null (never a 0-byte array), the stateful
> path emits a diagnostic, and the registration path surfaces an unresolved alias
> as a clean compile error that aborts the build (the `type_bridge.zig:270`
> Vector-lane `else => 0` is fixed the same way). Files:
> `src/ir/program_index.zig`, `src/ir/lower.zig`, `src/ir/type_bridge.zig`,
> `src/ir/type_resolver.zig`. Regressions:
> `examples/0143-types-typed-const-array-dim.sx` (typed-const dim direct + via
> alias for s64/string/struct, forward-ref alias, nested) and
> `examples/1129-diagnostics-array-dim-not-const.sx` (an unresolvable computed dim
> halts with a clean diagnostic + non-zero exit, not a fabricated 0-length array).
## Symptom ## Symptom
A fixed array whose dimension is a module-global integer constant (`N :: 16; A fixed array whose dimension is a module-global integer constant (`N :: 16;

View File

@@ -653,6 +653,24 @@ pub const Lowering = struct {
/// Pass 1: Scan declarations — register ASTs and extern stubs, but don't lower bodies. /// Pass 1: Scan declarations — register ASTs and extern stubs, but don't lower bodies.
fn scanDecls(self: *Lowering, decls: []const *const Node) void { 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| { for (decls) |decl| {
self.setCurrentSourceFile(decl.source_file); self.setCurrentSourceFile(decl.source_file);
const is_imported = if (self.main_file) |mf| 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; // 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); 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 {}; self.program_index.type_alias_map.put(cd.name, target_ty) catch {};
} else if (cd.value.data == .identifier) { } else if (cd.value.data == .identifier) {
// Identifier-RHS alias: MyAlias :: MyInt; WideAlias :: Wide; // 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: /// `[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 /// 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 /// / 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 /// compile-time integer is a hard error: emit a diagnostic so the driver
/// fabricating a 0 length, which gives a 0-byte array and out-of-bounds /// aborts (`hasErrors()`), then return a harmless `0` so body lowering
/// element access, issue 0083). /// finishes without touching the `.unresolved` sentinel (which would `@panic`
pub fn resolveArrayLen(self: *Lowering, len_node: *const Node) u32 { /// 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 (self.comptimeArrayDim(len_node)) |n| {
if (n < 0) { if (n < 0) {
if (self.diagnostics) |d| if (self.diagnostics) |d|
@@ -11673,10 +11703,10 @@ pub const Lowering = struct {
if (self.comptime_value_bindings) |cvb| { if (self.comptime_value_bindings) |cvb| {
if (cvb.get(name)) |v| return v; if (cvb.get(name)) |v| return v;
} }
if (self.program_index.module_const_map.get(name)) |ci| { // The module-const branch is shared verbatim with the stateless
if (ci.value.data == .int_literal) return ci.value.data.int_literal.value; // registration-time resolver (`type_bridge`) so a `[N]T` dimension
} // resolves to the same length on both paths (issue 0083).
return null; return program_index_mod.moduleConstInt(&self.program_index.module_const_map, name);
} }
/// Resolve a type node, checking type_bindings first for generic type params. /// Resolve a type node, checking type_bindings first for generic type params.

View File

@@ -40,6 +40,21 @@ pub const ModuleConstInfo = struct {
ty: TypeId, 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 }; pub const GlobalInfo = struct { id: inst.GlobalId, ty: TypeId };
/// Single lowering access point for declaration-name / import / visibility /// Single lowering access point for declaration-name / import / visibility

View File

@@ -41,30 +41,32 @@ const StatelessInner = struct {
return resolveAstType(node, self.table, self.alias_map, self.consts); return resolveAstType(node, self.table, self.alias_map, self.consts);
} }
/// Fixed-array dimension at registration time: a literal `[16]T`, or a /// 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. /// named module-global const `N :: 16; [N]T` (typed `N : s64 : 16` too)
/// Both yield the SAME length — registration-time paths (aliases, inline /// looked up in the const table. Both yield the SAME length — registration-
/// union/enum fields) must lay out a named-const dim identically to a literal /// time paths (aliases, inline union/enum fields) must lay out a named-const
/// (issue 0083). A dimension that is neither is not resolvable on this /// dim identically to a literal (issue 0083). Returns null when the dimension
/// binding-free path (it would be a computed/comptime expression, which the /// is neither (a computed/comptime expression, or a name not bound to an
/// stateful body-lowering path diagnoses as a hard error at the storage /// integer const). Null propagates to `resolveCompound`, which yields the
/// site); bail LOUDLY rather than fabricating a 0 length that silently gives a /// `.unresolved` sentinel rather than fabricating a 0 length that silently
/// 0-byte array and out-of-bounds element access. /// gives a 0-byte array and out-of-bounds element access; the registration
pub fn resolveArrayLen(self: StatelessInner, len_node: *const Node) u32 { /// caller surfaces the unresolved alias/type as a clean diagnostic.
pub fn resolveArrayLen(self: StatelessInner, len_node: *const Node) ?u32 {
switch (len_node.data) { 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, .identifier => |id| if (self.namedConstLen(id.name)) |n| return n,
.type_expr => |te| if (self.namedConstLen(te.name)) |n| return n, .type_expr => |te| if (self.namedConstLen(te.name)) |n| return n,
else => {}, 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 null;
return 0;
} }
/// 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 { fn namedConstLen(self: StatelessInner, name: []const u8) ?u32 {
const consts = self.consts orelse return null; const consts = self.consts orelse return null;
const ci = consts.get(name) orelse return null; const v = program_index_mod.moduleConstInt(consts, name) orelse return null;
if (ci.value.data == .int_literal) return @intCast(ci.value.data.int_literal.value); return if (v >= 0) @intCast(v) else null;
return null;
} }
}; };
@@ -265,10 +267,12 @@ fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTa
// Vector(N, T) is a built-in parameterized type // Vector(N, T) is a built-in parameterized type
if (std.mem.eql(u8, base_name, "Vector")) { if (std.mem.eql(u8, base_name, "Vector")) {
if (pt.args.len == 2) { if (pt.args.len == 2) {
const length: u32 = switch (pt.args[0].data) { // The lane count is a literal or a named module-const integer — the
.int_literal => |lit| @intCast(@as(u64, @bitCast(lit.value))), // same dimension forms a fixed array accepts. An unresolvable count
else => 0, // 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); const elem = resolveAstType(pt.args[1], table, alias_map, consts);
return table.vectorOf(elem, length); return table.vectorOf(elem, length);
} }

View File

@@ -23,10 +23,10 @@ const PrimInner = struct {
else => .unresolved, 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) { return switch (len_node.data) {
.int_literal => |lit| @intCast(lit.value), .int_literal => |lit| @intCast(lit.value),
else => 0, else => null,
}; };
} }
}; };

View File

@@ -95,10 +95,14 @@ pub const TypeResolver = struct {
const elem = inner.resolveInner(at.element_type); const elem = inner.resolveInner(at.element_type);
// The dimension is delegated to `inner` exactly like the element // The dimension is delegated to `inner` exactly like the element
// type: a literal `[16]T` and a named-const `N :: 16; [N]T` must // type: a literal `[16]T` and a named-const `N :: 16; [N]T` must
// produce the same length. The stateful resolver consults the // produce the same length. `resolveArrayLen` returns null when the
// const tables; the binding-free one handles literal dims (issue // dimension can't be resolved to a compile-time integer; that is
// 0083 — a 0 here gives a 0-byte array and OOB element access). // never a 0-length array (which gives a 0-byte alloca and OOB
const len = inner.resolveArrayLen(at.length); // 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); break :blk table.arrayOf(elem, len);
}, },
.function_type_expr => |ft| blk: { .function_type_expr => |ft| blk: {