fix(selector): spill rung stores before clobber — never reload a never-stored slot (#581) - #582
Merged
Merged
Conversation
…r-stored slot (#581) The direct selector's #253 add/sub (and bitwise/addr) immediate folds delete the const materialization BY source_line. Under the spill-on-exhaustion retry rung (VCR-RA-001 3b-lite), alloc_temp_or_spill for the const's temp first emits the victim's spill store `STR rX,[sp,#slot]` tagged with the SAME source_line — the fold deleted the store along with the MOVW, leaving the victim's vstack entry marked Spilled with no store. Its later reload read a never-written frame slot: silent wrong value on the shipped --relocatable path (spill_on_exhaust_242.wat hp(1,2) = 0xffffd1ce vs wasmtime 0x2e37). Fix: `is_const_materialization` (MOVW/MOVT/MVN) filters every by-source_line drop — drop_prev_const_materialization, splice_out_addr_const_materialization, and the #209 reciprocal-mult dead-divisor retain — so spill stores survive any fold. Defensive `assert_spill_reloads_have_stores` at the end of select_with_stack (internal-bug panic pattern): a reload from the reserved spill area without a preceding store to that slot can no longer leave the selector silently. Gates: new scripts/repro/spill_rung_581_differential.py (minimal fold-shape fixture + the original #580 discovery fixture, unicorn vs wasmtime, direct path) red on main → 12/12 green; unit fold_preserves_spill_store_581; frozen 3/3 bit-identical; r12_spill_496 + AAPCS oracles green; cargo test -p synth-synthesis -p synth-cli green; clippy -D warnings clean. Closes #581 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
avrabe
added a commit
that referenced
this pull request
Jul 2, 2026
… Belady spilling default-on (#585) Caps the four-lane arc: slot liveness (#579), exhaustion spill (#580), spill-rung fix (#582), SYNTH_SPILL_REALLOC flip + refreeze (#583). VCR-RA-001 -> verified; rivet release status v0.24.0: cuttable. Pin sweep + lock + CHANGELOG. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
avrabe
added a commit
that referenced
this pull request
Jul 8, 2026
…nst-div guards (#242, the PR #659 verdict) (#672) PR #659 held the SYNTH_SPILL_ON_EXHAUST flip on a measured i32-shape cycle regression and named the missing capability: post-exhaustion code quality on the optimized path. Root cause of the unreached allocation-time Belady slots: - fresh-monotonic slots defeat the overwrite-only frame-slot DCE (#515); - the eviction store's source is redefined immediately, defeating forward_stack_reloads; - spill_rechoice's rename deadness proof is segment-local — R2/R3 are never touched again on bridge streams, so "possibly live-out" declined them, and exact SegmentTrace equality rejected any fresh-register rename; - signed_div_const additionally paid for const-divisor trap guards the direct selector elides (#209 Opt 1a). Extensions, ALL scoped to functions the #580 machinery actually shaped (OptimizerBridge::spill_on_exhaust_fired — flag-on leaves every untouched function byte-identical, locked by vcr_ver_001_gate_242 + frozen 10/10 run in both flag states; flag-off is bit-for-bit the shipping pipeline): - scratch_dead_at: function-level R2/R3 exit-deadness (conservative at any branch/call/unmodeled op) feeding rename_kill_def; trace equality modulo provably-dead exit entries; per-pair pressure commit; byte-shrinking count-neutral folds admitted; - constant rematerialization of spilled single-instruction consts in spill_forward_segment (movt RMW kills the shape — the #582 discipline); - bounded fixpoint of the cleanup triple + late elide_dead_frame; - terminal-segment relaxed live-out pinning in range-realloc (only R0/R1 observable past bx lr pre-prologue; VCR-RA-003 validator run with the same exemptions; reload-free segments only); - const-divisor trap-guard elision in DivS/DivU/RemS/RemU (single-def Const scan, total-or-disabled def enumeration; c=0 keeps all, DivS c=-1 keeps the overflow guard). Cycle proxy (scripts/repro/postex_cycle_proxy.py, wasmtime-matched): spill_on_exhaust_242 +30.4%→+17.4%, spill_rung_581 +32.4%→+8.8%, high_pressure_i32 +8.0%→−24.0%, signed_div_const +120%→−33.3%, i64 pair/pool −16.4%/−2.9%. Two fixtures still miss ≤+5%: the residual is alloc_i32_scratch's fixed R4-R8 dest pool vs the direct selector's nine registers — the Track-A allocator replacement itself (see the gate doc's "residual, named" section; a reload-pool widening was tried and reverted, measured strictly worse). The flip stays HELD (#580). Gates: cargo test --workspace green; frozen anchors 10/10 + gate lock in BOTH flag states; r12_spill_496 / spill_on_exhaust_242 / i64_pair_exhaust_587 / i64_spill_pool_587 / spill_rung_581 differentials PASS flag-on; fmt + clippy -D warnings clean. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug (#581 — shipped-path silent miscompile, #518 severity class)
The direct selector's spill rung (the backend's exhaustion retry, VCR-RA-001 3b-lite) emitted a reload
ldr r0, [sp,#8]of a frame slot that was never stored:scripts/repro/spill_on_exhaust_242.watcompiled--relocatablereturns 0xffffd1ce forhp(1,2)where wasmtime says 0x2e37.Named root cause
The #253 add/sub (and bitwise/const-addr) immediate folds delete the const materialization by
source_line— which also deleted the spill store the const's own temp allocation emitted under the spill rung (alloc_temp_or_spill→spill_deepest_regtags the victim'sSTR rX,[sp,#slot]with the const's line), leaving the victim's vstack entry markedSpilledwith no store. The victim's register was then legitimately reassigned (themul.w-adjacent clobber the issue observed), and its later pop reloaded garbage from the never-written slot.Three sites shared the defect:
drop_prev_const_materialization(8 fold callers: add/sub/and/or/xor + const-addr loads)splice_out_addr_const_materialization(the wasm linear-memory access lowering: emit base+offset instead of movw+movt+ldr for constant addresses #95 store fold)retain(removed bysource_lineacross the whole function)Fix (store-before-clobber invariant restored)
is_const_materialization(MOVW/MOVT/MVN— the only ops the const handler emits) now filters every by-source_linedrop, so a spill store sharing the const's tag survives any fold. The materialization ops are emitted after the store, so the tail walk stops there naturally; the fold itself still fires (asserted in the new unit test).assert_spill_reloads_have_storesat the end ofselect_with_stack(the internal-bug panic pattern): a reload from the reserved spill area with no preceding store to that slot panics loudly instead of compiling wrong code. Gated onarea_reserved(v0.11.40 arm: spill-slot collision miscompiles dissolved k_mutex_unlock — live mutex-ptr arg clobbered by unpend() result, lock_count=0 store misses (silicon deadlock) #331 aliasing).Red → green
scripts/repro/spill_rung_581_differential.py(unicorn vs wasmtime, direct path, symbols from the ELF symtab) runs the new minimal fixturespill_rung_581.wat(7 live binop results +i32.const 77; i32.subfold — the smallest fold-deletes-store shape) plus the original #580 discovery fixture, 6 arg pairs each:hp(1,2)= 0x4e vs 0x51; originalhp(1,2)= 0xffffd1ce vs 0x2e37 (the recorded values)Gates
fold_preserves_spill_store_581(every spill-area reload has a matching store; the imm fold still drops theMOVW)frozen_codegen_bytes: ARM + RV32 + stack-fwd escape hatch) — the default pass never tags a spill store with a const's line, so shipped bytes are unchanged by constructionr12_spill_496_differential.pyPASS, AAPCS oracle (audit_aapcs_repro) 5/5cargo test -p synth-synthesis -p synth-cligreen (41 test binaries, 0 failures)cargo fmt --check+clippy --all-targets -D warningscleanScope note
The fix covers all three by-
source_linedeletion sites in the direct selector. The defensive validator is linear-scan (store-before-reload in emission order, which is how the vstack machinery always orders them) and is defense-in-depth behind the filters — slot reuse can mask a lost second-generation store from the scan, but no such deletion path remains.Closes #581
🤖 Generated with Claude Code