Files
sx/issues/0124-large-stack-array-aggregate-ops-crash-llvm.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.8 KiB

RESOLVED — 0124: 64K+ stack arrays emit whole-aggregate load/store ops that segfault LLVM

RESOLVED (2026-06-12). Root cause: two lowering sites materialized a local array as a first-class LLVM value, which the legalizer scalarizes into one SelectionDAG node per element. Fix: (1) lowerVarDecl (src/ir/lower/stmt.zig) emits NO store for an array-typed --- initializer — the slot stays uninitialized instead of receiving a whole-array undef store (tuple zero-init carve-out kept; non-array --- keeps the undef store); (2) lowerIndexExpr (src/ir/lower/expr.zig) reads elements of an array with addressable storage via index_gep on the storage + a single-element load — the general-expression sibling of 0110's lowerFor fix — without value-lowering the object (a dead whole-array load would still reach the DAG). Storage-less arrays (rvalues, by-value params) keep the index_get fallback. Residual sibling shapes filed as issue 0125 (any_to_string's per-array-type arms pass the array by value — any 64K+ array type + any {} print still crashes). Regression test: examples/0055-basic-large-stack-array.sx ([65536]u8 write/read loops + [131072]i64 first/last — sx build segfaulted pre-fix). 22 .ir snapshots re-pinned (removed undef stores / ig.tmp spills → in-place gep+load; reviewed instruction-shape-only). Gates: zig build test 426/426, suite 592/592, distribution repo 14/14.

Symptom

Declaring a large (~64KB+) stack array in a function reachable from main crashes the compiler during native emission — a segfault inside libLLVM, not a diagnostic.

  • Observed: Segmentation fault at address 0x16b... (a stack address) under sx build, inside DAGCombiner::visitMERGE_VALUESSelectionDAG::ReplaceAllUsesWith (via LLVMTargetMachineEmitToFile, src/ir/emit_llvm.zig:2894).
  • Expected: the program compiles; the array lives in the frame and is accessed in place.

The crash threshold is DAG-shape dependent, not a clean size boundary ([65535]u8 and [65537]u8 compile, [65536]u8, [66000]u8, [131072]u8 crash), because the real problem is the SelectionDAG node count: lowering materializes the array as a FIRST-CLASS LLVM value, and the legalizer scalarizes each whole-aggregate op into one node per element. Two emission shapes produce such ops:

  1. buf : [N]u8 = ---; stores a whole-array undef constant (store [N x i8] undef, ptr %alloca) — a store of nothing, for an explicitly-uninitialized local.
  2. buf[i] reads on a local array lower as index_get on the array VALUE: load the entire array as an SSA value, spill it to an ig.tmp alloca, GEP one element (the general-expression sibling of resolved issue 0110, which fixed only lowerFor's element fetch). Besides the crash, this copies N bytes to read 1.

Each shape crashes llc in isolation on the dumped IR; with both replaced by in-place access the module compiles.

Reproduction

#import "modules/std.sx";

f :: (fd: i32) {
    buf : [65536]u8 = ---;
    if buf[0] > 0 { out("x\n"); }
}

main :: () -> i32 {
    f(1);
    return 0;
}

Observed at master 7f2b8b5: sx build segfaults in libLLVM with the stack trace above. sx ir shows the two whole-aggregate ops:

%alloca1 = alloca [65536 x i8], align 1
store [65536 x i8] undef, ptr %alloca1, align 1
%load = load [65536 x i8], ptr %alloca1, align 1
%ig.tmp = alloca [65536 x i8], align 1
store [65536 x i8] %load, ptr %ig.tmp, align 1
%ig.ptr = getelementptr [65536 x i8], ptr %ig.tmp, i64 0, i64 0

Investigation prompt

Two lowering sites produce the whole-aggregate ops; fix both:

  1. src/ir/lower/stmt.zig lowerVarDecl (annotated branch): a .undef_literal initializer falls through to lowerExpr(val)constUndef(array type)store. --- means explicitly uninitialized — emit NO store at all (keep the existing tuple zero-init carve-out above it).
  2. src/ir/lower/expr.zig lowerIndexExpr: when the indexed object is an array with addressable storage (getExprAlloca hit, same guard as 0110's lowerFor fix), emit index_gep on the storage + a single-element load instead of index_get on the loaded array value. Storage-less arrays (rvalues) keep the index_get fallback. The object must NOT be lowered as a value on the storage path or the dead whole-array load still reaches the DAG.

Verification: the repro builds and runs (prints nothing or x depending on stack garbage — gate on exit 0 of the build, not the read); [65535]/[65537]/[131072] variants all build. Pin a regression example that builds AND deterministically runs (write before read). zig build && zig build test, bash tests/run_examples.sh green; expect .ir snapshot churn from removed undef stores and the new gep+load shape — re-pin and review.