Files
sx/issues/0079-global-array-element-store-dropped.md
agra 7306d37748 fix(ir): store to module-global array element targets live storage (issue 0079)
A store to a module-global array element (`g[i] = v`) was silently dropped:
a subsequent `g[i]` read the array's initializer, not `v`. Constant index,
variable index, and cross-function stores were all affected, in both `sx run`
and `sx build`. Global scalars and local arrays were fine.

Root cause: `Lowering.lowerExprAsPtr` (the lvalue/address path) handled only
local identifiers. A module-global identifier fell through to the value
fallback `lowerExpr`, which emits `global_get` — loading the whole array by
value. The LLVM backend's `emitIndexGep` then allocas a throwaway temp, copies
the value in, and GEPs into the temp, so the store wrote a discarded copy.

Fix: teach `lowerExprAsPtr`'s identifier arm about globals — emit `global_addr`
(a pointer into the global's live storage), or `global_get` for a pointer-typed
global (mirroring the local pointer case). Route the `address_of(index_expr)`
array base through `lowerExprAsPtr` too so `&g[i]` is likewise an lvalue into
the global. `index_gep` now GEPs directly into the global for const and variable
index, across functions. This also fixes global struct field stores, which
shared the same root cause.

Regression: examples/0136-types-global-array-element-store.sx (const-index,
var-index, cross-function store on a scalar global array; struct-element array
for stride; nested-array global for the recursive lvalue). Fails on the pre-fix
compiler, passes after.
2026-06-04 03:44:19 +03:00

103 lines
4.9 KiB
Markdown

# 0079 — stores to module-global array elements are silently dropped
> **RESOLVED.**
> **Root cause:** `Lowering.lowerExprAsPtr` (`src/ir/lower.zig`) — the lvalue/
> address path — handled only *local* identifiers (alloca pointers). A
> module-global identifier fell through to the value fallback `lowerExpr`,
> which emits `global_get` (loads the whole array *by value*). The LLVM
> backend's `emitIndexGep` then sees an array *value*, allocas a throwaway
> temp, copies the value in, and GEPs into the temp — so `g[i] = v` wrote a
> discarded copy and a later `g[i]` read the global's untouched initializer.
> Local arrays worked because they hit the alloca-pointer path; global
> *scalar* stores worked via `global_set`.
> **Fix:** teach `lowerExprAsPtr`'s identifier arm about globals — emit
> `global_addr` (a pointer into the global's live storage) for a normal
> global, or `global_get` for a pointer-typed global (mirroring the local
> pointer case). The same array-base resolution in the `address_of(index_expr)`
> path now routes through `lowerExprAsPtr` so `&g[i]` is also an lvalue into
> the global. `index_gep` then GEPs directly into `@g` for const AND variable
> index, across functions, in both `sx run` and `sx build`.
> **Regression:** `examples/0136-types-global-array-element-store.sx`
> (const-index, var-index, cross-function store on a scalar global array; a
> struct-element global array for element-stride; a nested-array global for the
> recursive indexed lvalue). FAILS on the pre-fix compiler (reads return the
> initializer / zero), PASSES after.
## Symptom
A store to a module-global (file-scope) ARRAY element is silently lost: after
`g[i] = v`, reading `g[i]` returns the array's INITIALIZER value, not `v`. No
diagnostic. Reproduces with a constant index, a variable index, and a store from
another function, in BOTH `sx run` (JIT) and `sx build` (AOT). Local-array stores
and global-SCALAR stores work correctly — so the indexed load/store on a *global*
array appears to read/write the initializer constant rather than the global's
live storage. This is a SILENT data-corruption bug (the dangerous class per the
project rules), not a crash.
Manager-reproduced output (JIT):
```
global[1] const-idx=20 (want 222)
global[k] var-idx=30 (want 333)
global[0] via fn=10 (want 111)
```
## Reproduction
```sx
#import "modules/std.sx";
g : [3]s64 = .[10, 20, 30];
write_global :: (i: s64, v: s64) { g[i] = v; }
main :: () {
loc : [3]s64 = .[10, 20, 30];
loc[1] = 222;
print("local[1]={}\n", loc[1]); // 222 (correct)
g[1] = 222;
print("global[1] const-idx={}\n", g[1]); // 20 (WRONG, want 222)
k := 2;
g[k] = 333;
print("global[k] var-idx={}\n", g[k]); // 30 (WRONG, want 333)
write_global(0, 111);
print("global[0] via fn={}\n", g[0]); // 10 (WRONG, want 111)
}
```
`./zig-out/bin/sx run <file>` → prints the WRONG values above. Expected:
`local[1]=222 / global[1]=222 / global[k]=333 / global[0]=111`.
## Investigation prompt
A store to a module-global array element (`g[i] = v`) is silently lost: a
subsequent `g[i]` yields the initializer value, in BOTH the JIT (`sx run`) and
compiled (`sx build`) paths. Global SCALAR stores and LOCAL array stores both
work, so the defect is specific to INDEXED lvalue access on a GLOBAL array.
Suspect the IR-gen / codegen for an indexed expression on a global symbol:
the load of `global_array[idx]` is likely decaying / constant-folding to the
array's initializer constant (or the address computation for the store targets a
private copy of the initializer rather than the global's live storage). Look in
the index-expression lowering and global-symbol address resolution (and how
global array initializers are materialized) — `src/ir/lower.zig` /
`src/ir/emit_llvm.zig` and the global-symbol/constant-initializer paths.
The fix must make `load(global_array[idx])` read the global's live storage and
`store(global_array[idx], v)` write it — for constant AND variable indices, and
when the store happens in a different function than the read. Do NOT special-case;
fix the address resolution so a global array element is an lvalue into the
global's storage like any other.
## Verification (once fixed)
- The repro above prints `222 / 333 / 111` for the global cases (exit 0).
- Add a pinned regression `examples/NNNN-*.sx` covering const-index, var-index,
and cross-function store to a global array (+ a global array of a struct/larger
element if practical).
- `zig build && zig build test && bash tests/run_examples.sh` all green.
## Provenance
Discovered by the `distribution` flow (sx-foundation step F3.1, std.cli argv
accessor) while exploring argv backing strategies. The shipped F3.1 code does NOT
depend on this (it uses caller-provided buffers + zero-copy views over the C argv
block), so F3.1 is correct independently — this is a separate latent
silent-miscompile surfaced per the STOP-on-compiler-bug rule.