From d2bf8f3f2d912e1f50e35382f8a08e7192e78b0b Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 4 Jun 2026 09:39:18 +0300 Subject: [PATCH] fix(ir): unify named-const array-dim resolution + kill length-0 fabrication (0083) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- examples/0143-types-typed-const-array-dim.sx | 65 +++++++++++++++++++ .../1129-diagnostics-array-dim-not-const.sx | 20 ++++++ .../0143-types-typed-const-array-dim.exit | 1 + .../0143-types-typed-const-array-dim.stderr | 1 + .../0143-types-typed-const-array-dim.stdout | 6 ++ .../1129-diagnostics-array-dim-not-const.exit | 1 + ...129-diagnostics-array-dim-not-const.stderr | 5 ++ ...129-diagnostics-array-dim-not-const.stdout | 1 + ...named-const-array-dimension-miscompiled.md | 24 +++++++ src/ir/lower.zig | 46 ++++++++++--- src/ir/program_index.zig | 15 +++++ src/ir/type_bridge.zig | 44 +++++++------ src/ir/type_resolver.test.zig | 4 +- src/ir/type_resolver.zig | 12 ++-- 14 files changed, 211 insertions(+), 34 deletions(-) create mode 100644 examples/0143-types-typed-const-array-dim.sx create mode 100644 examples/1129-diagnostics-array-dim-not-const.sx create mode 100644 examples/expected/0143-types-typed-const-array-dim.exit create mode 100644 examples/expected/0143-types-typed-const-array-dim.stderr create mode 100644 examples/expected/0143-types-typed-const-array-dim.stdout create mode 100644 examples/expected/1129-diagnostics-array-dim-not-const.exit create mode 100644 examples/expected/1129-diagnostics-array-dim-not-const.stderr create mode 100644 examples/expected/1129-diagnostics-array-dim-not-const.stdout diff --git a/examples/0143-types-typed-const-array-dim.sx b/examples/0143-types-typed-const-array-dim.sx new file mode 100644 index 0000000..b29831d --- /dev/null +++ b/examples/0143-types-typed-const-array-dim.sx @@ -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); +} diff --git a/examples/1129-diagnostics-array-dim-not-const.sx b/examples/1129-diagnostics-array-dim-not-const.sx new file mode 100644 index 0000000..06060c3 --- /dev/null +++ b/examples/1129-diagnostics-array-dim-not-const.sx @@ -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]); +} diff --git a/examples/expected/0143-types-typed-const-array-dim.exit b/examples/expected/0143-types-typed-const-array-dim.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0143-types-typed-const-array-dim.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0143-types-typed-const-array-dim.stderr b/examples/expected/0143-types-typed-const-array-dim.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0143-types-typed-const-array-dim.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0143-types-typed-const-array-dim.stdout b/examples/expected/0143-types-typed-const-array-dim.stdout new file mode 100644 index 0000000..c92dfb8 --- /dev/null +++ b/examples/expected/0143-types-typed-const-array-dim.stdout @@ -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 diff --git a/examples/expected/1129-diagnostics-array-dim-not-const.exit b/examples/expected/1129-diagnostics-array-dim-not-const.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1129-diagnostics-array-dim-not-const.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1129-diagnostics-array-dim-not-const.stderr b/examples/expected/1129-diagnostics-array-dim-not-const.stderr new file mode 100644 index 0000000..5b169d8 --- /dev/null +++ b/examples/expected/1129-diagnostics-array-dim-not-const.stderr @@ -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; + | ^^^^^^^^^^ diff --git a/examples/expected/1129-diagnostics-array-dim-not-const.stdout b/examples/expected/1129-diagnostics-array-dim-not-const.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1129-diagnostics-array-dim-not-const.stdout @@ -0,0 +1 @@ + diff --git a/issues/0083-named-const-array-dimension-miscompiled.md b/issues/0083-named-const-array-dimension-miscompiled.md index 73ab66e..1c6ee31 100644 --- a/issues/0083-named-const-array-dimension-miscompiled.md +++ b/issues/0083-named-const-array-dimension-miscompiled.md @@ -25,6 +25,30 @@ > `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 / > 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 A fixed array whose dimension is a module-global integer constant (`N :: 16; diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 5045547..175a189 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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 :: []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. diff --git a/src/ir/program_index.zig b/src/ir/program_index.zig index a90d04c..a93cf75 100644 --- a/src/ir/program_index.zig +++ b/src/ir/program_index.zig @@ -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 diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index fc05de9..e93ec64 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -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); } diff --git a/src/ir/type_resolver.test.zig b/src/ir/type_resolver.test.zig index ad51f8f..0d415bb 100644 --- a/src/ir/type_resolver.test.zig +++ b/src/ir/type_resolver.test.zig @@ -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, }; } }; diff --git a/src/ir/type_resolver.zig b/src/ir/type_resolver.zig index 7a710d9..e9075e3 100644 --- a/src/ir/type_resolver.zig +++ b/src/ir/type_resolver.zig @@ -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: {