From 12552e125d29a7e0cef1f73c30586f613d78dfa7 Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 4 Jun 2026 08:22:45 +0300 Subject: [PATCH] fix(ir): resolve named-const array dims (0083) + materialize literal slice args (0084) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two silent-miscompile codegen fixes: 0083 — named-const array dimension. `TypeResolver.resolveCompound`'s array arm resolved the dimension with `if int_literal ... else 0`, so a named const (`N :: 16; [N]T`) hit the silent `else 0`: the array became 0-length / 0-byte and element access ran out of bounds (garbage for scalars, bus error for slice/pointer/struct elements). The arm now delegates the dimension to `inner.resolveArrayLen` (symmetric with `inner.resolveInner` for the element). The stateful `Lowering.resolveArrayLen` evaluates it as a compile-time integer across the comptime-constant / generic-value / module-global const tables and emits a diagnostic — no fabricated length — when it isn't one. 0084 — `.[...]` literal passed directly as a call arg. `lowerArrayLiteral` always yields an aggregate array value; the array→slice conversion is the caller's job. The local-bound var-decl path did it, but the call-arg coercion path had no array→slice arm, so `classify([N]T, []T)` returned `.none` and the raw array was passed where a slice was expected (callee read its {ptr,len} header off the wrong bytes → 0 / garbage / segfault). `classify` now returns a new `.array_to_slice` plan for same-element `[N]T → []T`, and `coerceToType` emits the existing `array_to_slice` op — identical to the local-bound path. Regressions (fail-before/pass-after demonstrated on the pre-fix compiler): examples/0140-types-named-const-array-dim.sx (s64 + string + struct elems) examples/0141-types-slice-literal-direct-call-arg.sx (string + []s64) Gate: zig build, zig build test, bash tests/run_examples.sh (387 passed). Issues 0083 and 0084 marked RESOLVED. --- examples/0140-types-named-const-array-dim.sx | 32 ++++++++++++ ...141-types-slice-literal-direct-call-arg.sx | 34 +++++++++++++ .../0140-types-named-const-array-dim.exit | 1 + .../0140-types-named-const-array-dim.stderr | 1 + .../0140-types-named-const-array-dim.stdout | 3 ++ ...1-types-slice-literal-direct-call-arg.exit | 1 + ...types-slice-literal-direct-call-arg.stderr | 1 + ...types-slice-literal-direct-call-arg.stdout | 4 ++ ...named-const-array-dimension-miscompiled.md | 42 ++++++++++++++++ ...ice-literal-direct-call-arg-miscompiled.md | 43 ++++++++++++++++ src/ir/conversions.zig | 15 ++++++ src/ir/lower.zig | 50 +++++++++++++++++++ src/ir/type_bridge.zig | 12 +++++ src/ir/type_resolver.test.zig | 6 +++ src/ir/type_resolver.zig | 7 ++- 15 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 examples/0140-types-named-const-array-dim.sx create mode 100644 examples/0141-types-slice-literal-direct-call-arg.sx create mode 100644 examples/expected/0140-types-named-const-array-dim.exit create mode 100644 examples/expected/0140-types-named-const-array-dim.stderr create mode 100644 examples/expected/0140-types-named-const-array-dim.stdout create mode 100644 examples/expected/0141-types-slice-literal-direct-call-arg.exit create mode 100644 examples/expected/0141-types-slice-literal-direct-call-arg.stderr create mode 100644 examples/expected/0141-types-slice-literal-direct-call-arg.stdout create mode 100644 issues/0083-named-const-array-dimension-miscompiled.md create mode 100644 issues/0084-slice-literal-direct-call-arg-miscompiled.md diff --git a/examples/0140-types-named-const-array-dim.sx b/examples/0140-types-named-const-array-dim.sx new file mode 100644 index 0000000..a17a519 --- /dev/null +++ b/examples/0140-types-named-const-array-dim.sx @@ -0,0 +1,32 @@ +// A fixed array whose dimension is a module-global named constant +// (`N :: 16; [N]T`) has the same layout as a literal-dimension array +// (`[16]T`): correct length and element stride for scalar, slice/pointer +// (string), and struct element types. +// Regression (issue 0083): a named-const dim resolved to length 0, giving a +// 0-byte alloca — scalar reads returned garbage and string/struct elements +// bus-errored. +#import "modules/std.sx"; + +N :: 4; + +P :: struct { x: s64; y: s64; } + +main :: () { + // Scalar elements: store then read back. + a : [N]s64 = ---; + a[0] = 7; + a[3] = 42; + print("scalar a0={} a3={}\n", a[0], a[3]); + + // Slice/pointer elements (string): used to bus-error. + s : [N]string = ---; + s[0] = "hi"; + s[1] = "yo"; + print("string s0={} s1={}\n", s[0], s[1]); + + // Struct elements. + ps : [N]P = ---; + ps[0] = P.{ x = 1, y = 2 }; + ps[2] = P.{ x = 5, y = 6 }; + print("struct p0x={} p0y={} p2x={}\n", ps[0].x, ps[0].y, ps[2].x); +} diff --git a/examples/0141-types-slice-literal-direct-call-arg.sx b/examples/0141-types-slice-literal-direct-call-arg.sx new file mode 100644 index 0000000..8351d6b --- /dev/null +++ b/examples/0141-types-slice-literal-direct-call-arg.sx @@ -0,0 +1,34 @@ +// A `.[...]` array/slice literal passed DIRECTLY as a call argument behaves +// identically to binding it to a typed local first: the literal is +// materialized into addressable storage and a {ptr,len} slice header is built +// over it, so the callee reads the element CONTENTS correctly. +// Regression (issue 0084): a direct literal arg passed the raw array value +// where a slice was expected, so the callee read its header off the wrong +// bytes and returned garbage (0). +#import "modules/std.sx"; + +count_nope :: (xs: []string) -> s64 { + n := 0; + i := 0; + while i < xs.len { if xs[i] == "nope" { n += 1; } i += 1; } + return n; +} + +sum :: (xs: []s64) -> s64 { + s := 0; + i := 0; + while i < xs.len { s += xs[i]; i += 1; } + return s; +} + +main :: () { + // string slice: direct literal vs local-bound — both see 2 "nope"s. + print("str direct={}\n", count_nope(.["a", "nope", "b", "nope"])); + local : []string = .["a", "nope", "b", "nope"]; + print("str local={}\n", count_nope(local)); + + // numeric slice: direct literal vs local-bound — both sum to 100. + print("num direct={}\n", sum(.[10, 20, 30, 40])); + nums : []s64 = .[10, 20, 30, 40]; + print("num local={}\n", sum(nums)); +} diff --git a/examples/expected/0140-types-named-const-array-dim.exit b/examples/expected/0140-types-named-const-array-dim.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0140-types-named-const-array-dim.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0140-types-named-const-array-dim.stderr b/examples/expected/0140-types-named-const-array-dim.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0140-types-named-const-array-dim.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0140-types-named-const-array-dim.stdout b/examples/expected/0140-types-named-const-array-dim.stdout new file mode 100644 index 0000000..9d6220b --- /dev/null +++ b/examples/expected/0140-types-named-const-array-dim.stdout @@ -0,0 +1,3 @@ +scalar a0=7 a3=42 +string s0=hi s1=yo +struct p0x=1 p0y=2 p2x=5 diff --git a/examples/expected/0141-types-slice-literal-direct-call-arg.exit b/examples/expected/0141-types-slice-literal-direct-call-arg.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0141-types-slice-literal-direct-call-arg.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0141-types-slice-literal-direct-call-arg.stderr b/examples/expected/0141-types-slice-literal-direct-call-arg.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0141-types-slice-literal-direct-call-arg.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0141-types-slice-literal-direct-call-arg.stdout b/examples/expected/0141-types-slice-literal-direct-call-arg.stdout new file mode 100644 index 0000000..d723da5 --- /dev/null +++ b/examples/expected/0141-types-slice-literal-direct-call-arg.stdout @@ -0,0 +1,4 @@ +str direct=2 +str local=2 +num direct=100 +num local=100 diff --git a/issues/0083-named-const-array-dimension-miscompiled.md b/issues/0083-named-const-array-dimension-miscompiled.md new file mode 100644 index 0000000..1b75f26 --- /dev/null +++ b/issues/0083-named-const-array-dimension-miscompiled.md @@ -0,0 +1,42 @@ +# 0083 — fixed array with a named-constant dimension is miscompiled + +> **RESOLVED.** Root cause: `TypeResolver.resolveCompound`'s array arm resolved +> the dimension with `if (length.data == .int_literal) ... else 0` — a named +> const (`N :: 16`) hit the silent `else 0`, so `[N]T` became a 0-length / 0-byte +> array and element access ran out of bounds (garbage for scalars, bus error for +> slice/pointer/struct elements). Fix: the array arm now delegates the dimension +> to `inner.resolveArrayLen` (symmetric with `inner.resolveInner` for the element +> type). The stateful `Lowering.resolveArrayLen` evaluates the dimension as a +> compile-time integer across the comptime-constant, generic-value, and +> module-global const tables, and emits a diagnostic (no fabricated length) when +> it isn't one. Files: `src/ir/type_resolver.zig`, `src/ir/lower.zig`, +> `src/ir/type_bridge.zig`. Regression: `examples/0140-types-named-const-array-dim.sx` +> (s64 + string + struct element types). + +## Symptom +A fixed array whose dimension is a module-global integer constant (`N :: 16; +a : [N]T`) miscompiles element access: reads/writes compute a wrong address. +With `s64` elements `a[0]` returns GARBAGE (silent); with slice/pointer element +types (`[N]string`) it Bus-errors. The identical program with a LITERAL dimension +(`a : [16]T`) is correct. Silent-miscompile class (cf. 0079–0082). + +## Reproduction +```sx +#import "modules/std.sx"; +N :: 16; +main :: () { a : [N]s64 = ---; a[0] = 7; print("a0={}\n", a[0]); } +``` +`./zig-out/bin/sx run` prints `a0=8472789232` (garbage); want `a0=7`. Replacing +`[N]` with `[16]` prints `7`. + +## Investigation prompt +A fixed-array TYPE whose dimension is a named const (`N :: 16; [N]T`) resolves to +a wrong element stride / array length in codegen — element address computation is +wrong (garbage for scalars, bad pointer for slice/pointer elements). Literal +dimensions are correct, so the defect is in resolving the array-type DIMENSION +from a constant expression (vs a literal) — the dim likely resolves to 0/unknown +or the element size is wrong. Look at array-type resolution where the length is a +const-expr (type lowering / sizeof / element-stride computation). Fix so a +named-const dimension yields the same layout as the literal. Verify with the +repro (expect 7) + a `[N]string`/`[N]struct` case (no bus error, correct reads), +and `zig build && zig build test && bash tests/run_examples.sh` green. diff --git a/issues/0084-slice-literal-direct-call-arg-miscompiled.md b/issues/0084-slice-literal-direct-call-arg-miscompiled.md new file mode 100644 index 0000000..0a6ec71 --- /dev/null +++ b/issues/0084-slice-literal-direct-call-arg-miscompiled.md @@ -0,0 +1,43 @@ +# 0084 — array/slice literal passed directly as a call argument miscompiles + +> **RESOLVED.** Root cause: `lowerArrayLiteral` always produces an aggregate +> ARRAY value; the array→slice conversion is the caller's job. The local-bound +> var-decl path did it (emits `array_to_slice`), but the call-argument coercion +> path (`coerceCallArgs` → `coerceToType` → `CoercionResolver.classify`) had no +> array→slice arm, so `classify([N]T, []T)` returned `.none` and the raw array +> value was passed where a slice was expected — the callee read its {ptr,len} +> header off the wrong bytes (returned 0 / garbage, segfaulted for `[]s64`). Fix: +> `classify` now returns a new `.array_to_slice` plan for `[N]T → []T` (same +> element type), and `coerceToType` emits the existing `array_to_slice` op, which +> materializes the array into addressable storage and builds the slice header — +> identical to the local-bound path. Files: `src/ir/conversions.zig`, +> `src/ir/lower.zig`. Regression: `examples/0141-types-slice-literal-direct-call-arg.sx` +> (string + numeric `[]s64`, direct vs local-bound). + +## Symptom +A `.[...]` array/slice literal passed DIRECTLY as a call argument yields a slice +whose element CONTENTS are not reliably readable in the callee (silent — reads +garbage, wrong results). Binding the same literal to a typed local first and +passing the local is correct. + +## Reproduction +```sx +#import "modules/std.sx"; +show :: (xs: []string) -> s64 { n:=0; i:=0; while i |lit| lit.value, + .identifier => |id| self.comptimeIntNamed(id.name), + .type_expr => |te| self.comptimeIntNamed(te.name), + else => null, + }; + } + + /// Resolve a name to a compile-time integer across the three const tables. + fn comptimeIntNamed(self: *Lowering, name: []const u8) ?i64 { + if (self.comptime_constants.get(name)) |cv| switch (cv) { + .int_val => |iv| return iv, + else => {}, + }; + 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; + } + /// Resolve a type node, checking type_bindings first for generic type params. pub fn resolveTypeWithBindings(self: *Lowering, node: *const Node) TypeId { // Pack-index in a type position: `$[]` resolves to the @@ -13984,6 +14033,7 @@ pub const Lowering = struct { .ptr_int_bitcast => return self.builder.emit(.{ .bitcast = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty), .narrow => return self.builder.emit(.{ .narrow = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty), .widen => return self.builder.emit(.{ .widen = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty), + .array_to_slice => return self.builder.emit(.{ .array_to_slice = .{ .operand = val } }, dst_ty), } } diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index 6145395..00711f2 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -28,6 +28,18 @@ const StatelessInner = struct { pub fn resolveInner(self: StatelessInner, node: *const Node) TypeId { return resolveAstType(node, self.table, self.alias_map); } + /// 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. + pub fn resolveArrayLen(self: StatelessInner, len_node: *const Node) u32 { + _ = self; + return switch (len_node.data) { + .int_literal => |lit| @intCast(lit.value), + else => 0, + }; + } }; // ── AST Node → TypeId ─────────────────────────────────────────────────── diff --git a/src/ir/type_resolver.test.zig b/src/ir/type_resolver.test.zig index e16562f..ad51f8f 100644 --- a/src/ir/type_resolver.test.zig +++ b/src/ir/type_resolver.test.zig @@ -23,6 +23,12 @@ const PrimInner = struct { else => .unresolved, }; } + pub fn resolveArrayLen(_: PrimInner, len_node: *const Node) u32 { + return switch (len_node.data) { + .int_literal => |lit| @intCast(lit.value), + else => 0, + }; + } }; test "TypeResolver.resolvePrimitive maps builtin keywords, null otherwise" { diff --git a/src/ir/type_resolver.zig b/src/ir/type_resolver.zig index 2cb7166..7a710d9 100644 --- a/src/ir/type_resolver.zig +++ b/src/ir/type_resolver.zig @@ -93,7 +93,12 @@ pub const TypeResolver = struct { .optional_type_expr => |ot| table.optionalOf(inner.resolveInner(ot.inner_type)), .array_type_expr => |at| blk: { const elem = inner.resolveInner(at.element_type); - const len: u32 = if (at.length.data == .int_literal) @intCast(at.length.data.int_literal.value) else 0; + // 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); break :blk table.arrayOf(elem, len); }, .function_type_expr => |ft| blk: {