Files
sx/issues/0079-global-array-element-store-dropped.md
agra d8076b9333 lang: rename signed integer types sN -> iN
Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.

Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).

Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.

zig build test: 426/426; examples suite: 595/595.
2026-06-12 09:31:53 +03:00

4.9 KiB

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

#import "modules/std.sx";

g : [3]i64 = .[10, 20, 30];
write_global :: (i: i64, v: i64) { g[i] = v; }

main :: () {
    loc : [3]i64 = .[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.