A constant-FOLDABLE expression array dimension (`[M + 1]`, `[M * N]`, `[N - M]`, nested `[M + N - 1]`, parenthesised `[(M + 1) * 2]`, mixing untyped and typed module consts) was wrongly rejected as "not a compile-time integer constant" even though every operand is compile-time-known. Attempts 1-3 resolved only a bare named-const dim or a literal; an expression dim must be EVALUATED, not rejected. Fix: the shared dim resolver now routes the dimension through a single constant integer-expression evaluator (`program_index.evalConstIntExpr`) that folds integer `+ - * / %` and unary negate over literals and named/typed module consts, recursively (parentheses carry no AST node). The leaf-name lookup is delegated via `ctx.lookupDimName`, so the stateful body-lowering path (`Lowering`, which also sees comptime constants and generic `$N` values) and the stateless registration path (`type_bridge.StatelessInner`, module consts only) share the EXACT SAME folding logic and cannot diverge — an expression dim via a type alias resolves identically to the direct form. No-fabrication discipline unchanged: a genuinely non-comptime dimension (runtime local, non-comptime call, unbound name) or arithmetic that overflows / divides by zero still yields null -> `.unresolved` -> the same clean compile-halting diagnostic, never a fabricated length. - examples/0144-types-const-expr-array-dim.sx: every expression form, direct vs alias, scalar / string / struct element types (fails on the pre-fix compiler, passes after). - examples/1129 re-pointed at a genuinely non-const dimension (`[get()]s64`, a runtime call) so it still proves the stateless clean-halt (a foldable expression is no longer an error). - program_index.test.zig: unit test for evalConstIntExpr folding and clean-halt-on-non-const.
103 lines
6.6 KiB
Markdown
103 lines
6.6 KiB
Markdown
# 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.
|
||
>
|
||
> **Exhaustive follow-up (attempt 2).** The first fix covered every *stateful*
|
||
> resolution path (direct local decls, struct fields, function params/returns),
|
||
> but the *stateless* registration-time resolver (`type_bridge`, used for type
|
||
> aliases `Arr :: [N]T` and inline union/enum field types) still resolved the
|
||
> named dim with a silent `else 0` — so `Arr :: [N]s64; a : Arr` and
|
||
> `union { a: [N]s64 }` were still miscompiled. Fix: the module-global const
|
||
> table (`ProgramIndex.module_const_map`) is now threaded 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 dimension on the binding-free path) bails LOUDLY instead of
|
||
> fabricating a 0 length. Files: `src/ir/type_resolver.zig`, `src/ir/lower.zig`,
|
||
> `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).
|
||
>
|
||
> **Const-expression dimensions (attempt 4).** Attempts 1–3 resolved only a BARE
|
||
> named-const dim (`[M]`) or a literal (`[5]`); any constant-FOLDABLE *expression*
|
||
> dimension (`[M + 1]`, `[M * N]`, `[N - M]`, nested `[M + N - 1]`, parenthesised
|
||
> `[(M + 1) * 2]`) was wrongly rejected as "not a compile-time integer constant"
|
||
> even though every operand is compile-time-known. Such a dimension MUST be
|
||
> evaluated, not rejected. Fix: the shared dim resolver now routes the dimension
|
||
> through a single constant integer-expression evaluator
|
||
> (`program_index.evalConstIntExpr`) that folds integer `+ - * / %` and unary
|
||
> negate (parentheses carry no AST node) over literals and named/typed module
|
||
> consts, recursively. The leaf-name lookup is delegated (`ctx.lookupDimName`) so
|
||
> the stateful body-lowering path and the stateless registration path share the
|
||
> EXACT SAME folding logic and cannot diverge — an expression dim via a type alias
|
||
> resolves identically to the direct form. The no-fabrication discipline is
|
||
> unchanged: a genuinely non-comptime dimension (a runtime local, a non-comptime
|
||
> call, an unbound name) — or arithmetic that overflows / divides by zero — still
|
||
> yields null → `.unresolved` → the same clean compile-halting diagnostic, never a
|
||
> fabricated length. Files: `src/ir/program_index.zig` (+`.test.zig`),
|
||
> `src/ir/lower.zig`, `src/ir/type_bridge.zig`. Regression:
|
||
> `examples/0144-types-const-expr-array-dim.sx` (every expression form, direct vs
|
||
> alias, scalar / string / struct element types); `1129` re-pointed at a genuinely
|
||
> non-const dimension (`[get()]s64`, a runtime call) so it still proves the
|
||
> stateless clean-halt.
|
||
|
||
## 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.
|