← board

NilPy/uforth: managed-string hidden temp released with garbage at method return

Hangs off [[feature-nilpy-corpus-uforth]] (runtime-correctness phase). Blocks uforth actually running: it now compiles, constructs, prints its banner, reaches the REPL, reads a line, tokenizes it — and SIGSEGVs when VM.tokenize returns.

The crash (decisive gdb evidence)

Program received signal SIGSEGV
rip  0x40021a:  decq  -0x10(%rax)      ; managed-string ARC refcount decrement
                jne   0x400244
                sub   $0x10,%rax
rax  0x200000000                       ; a GARBAGE handle (not a heap pointer)

decq -0x10(%rax); jne; sub $0x10,%rax is the managed AnsiString release sequence (decrement the refcount word at handle-0x10, free if zero). rax = 0x200000000 is not a valid heap pointer, so [rax-0x10] faults. So: a value typed as a managed string is being RELEASED at scope exit while holding stack garbage.

What is known

Diagnosis

Matches the known landmine class [[project_interface_ascast_temp_lifetime_landmine]]: an IR-lowering hidden temp of managed-string type MISSES prologue zero-init, so a skipped branch / scope exit releases stack garbage — a layout-sensitive SIGSEGV. The suspect temp is created somewhere in tokenize's lowering (most likely the nested-def capture spill for flush_current, given the early-return still crashes, or the managed-string ternary/join result temp) and is not covered by EmitManagedLocalsZeroInit / SymIsHiddenArgTemp.

KEY FINDING: optimizer-sensitive (-O2 only)

At -O0 (-g) and -O1 the tokenize SIGSEGV does NOT happen — uforth runs past tokenize and reaches a different, deterministic error deeper in (ValueError: byte slice assignment length mismatch (expected 8, got <garbage>), in set_in_pos's int(pos).to_bytes(8,...) slice-assign — a second uninitialized-value bug, tracked separately). This CONFIRMS the diagnosis: the managed-string hidden temp is genuinely uninitialized, and only -O2's register allocator / stack-slot reuse leaves garbage (0x200000000) in it; -O0 happens to leave zero. So the fix is real prologue zero-init of that temp, not an -O2 codegen bug per se. Default builds are -O2, so this still blocks.

Refined findings (2026-07-21, session 2)

Analysis caveat (don't repeat this dead end)

An IR-dump count of default_mem (12) vs unnamed tyAnsiString temps (13) in tokenize looked like a smoking gun (sym 153, the str(tokens[-1]).upper() if tokens else "" ternary temp, line 111). It is NOT conclusive: default_mem is the INLINE release-before-store for arg temps, whereas ternary/result temps are nil'd via SymIsHiddenArgTemp at the CODEGEN prologue — which the IR dump does not show. So a temp without default_mem may still be nil'd by the flag. The exact construct (str-ternary with a str-method-chain arm + early return) reproduces GREEN in isolation. The bug stays layout-sensitive.

Tooling for the next attempt

Raw gdb conditional breakpoints on the ARC release helper (0x40021a decq -0x10(%rax)) are too slow — the condition is evaluated on every managed release across uforth's whole startup (thousands). Use instead: (a) rr record/replay with a hardware watchpoint on the faulting frame slot, or (b) a debug compiler build that logs each managed temp's frame offset + whether it was nil-inited, then diff against the epilogue release list for tokenize. The gdb helper addresses: AddRef 0x400202 (incq -0x10(%rax)), Release ~0x400210 (decq -0x10(%rax)).

Next steps

Repro (compiles, crashes at runtime)

~/projects/uforth/uforth.py compiled to a .npy: echo "1 2 + ." | ./ufSIGSEGV. Reaches the banner "Unicode Forth (UF/O)" first.