Skip to content

feat(vcr-ra): i64 pair-spill via param frame-backing — VCR-RA-001 acceptance closes (v0.11.40) - #325

Merged
avrabe merged 1 commit into
mainfrom
feat/vcr-ra-pair-spill
Jun 11, 2026
Merged

avrabe merged 1 commit into
mainfrom
feat/vcr-ra-pair-spill

Conversation

@avrabe

@avrabe avrabe commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

The last criterion

The #242 acceptance review left one gap: "a high-pressure i64 module compiles with no hard-fail." Investigation revised the diagnosis: the pair-spill loop already existed (#171) — the true blocker was pinned param home registers (#193's reserved, up to R0–R3) which no stack spill can free. The fix is a third retry mode that forces the proven #204 param frame-backing onto call-free functions, emptying the reserve; pigeonhole over the freed pool then guarantees a consecutive pair.

Backend retry ladder (each pass only on its predecessor's exact Err, so bit-identity stays structural): default → spill-only (#320) → spill+param-backing (this PR).

Evidence

VCR-RA-001 status after this merges: all five criteria met

compile-no-hard-fail ✅ (both pressure classes) · fixtures bit-identical ✅ · cycles equal-or-better ✅ (gale silicon) · cost-gate revert ✅ (executed, #322) · validator guarding ✅ (#323). Honest bounds named: the 8-slot spill pool, the distinct R8-pair/i64-param-convention class (epic-tracked), stack-passed params.

Release: v0.11.40 — the allocator's acceptance release.

🤖 Generated with Claude Code

…iterion closes (#242)

The last hard-fail named by the v0.11.40 acceptance review: a high-pressure
i64 module now compiles with no hard-fail.

ROOT CAUSE (differs from the review's reading): the pair allocator
(alloc_consecutive_pair) has ALWAYS spilled register-resident stack values —
pair-aware, both halves into one 8-byte slot (#171). The remaining
"no consecutive pair of free registers for i64" Err fires only when the
operand stack holds nothing register-resident and the blockers are the
PINNED PARAM HOME REGISTERS (#193 `reserved`, up to r0-r3) plus the popped
operand pairs (`extra_avoid`, up to 4 regs) — 8 of the 9-register pool, no
pair left, and no amount of stack spilling can free a param home register.

FIX (same structural-bit-identity pattern as #320): a third retry mode,
`set_param_backing_on_exhaustion`, forces the proven #204 `param_slots`
frame-backing for call-free functions — params spill to frame slots at entry
and reload on read, so `reserved` empties and a free consecutive pair always
exists after stack spilling (extra_avoid is at most two adjacent pairs among
nine registers; pigeonhole over the free segments). The backend's
select_direct ladder: pass 1 default → pass 2 spill-only (#320, on the
single-reg Err) → pass 3 spill+param-backing (on the pair Err). Functions
that compile on an earlier pass are selected by exactly yesterday's code.
Loop bound unchanged: each spill_deepest_reg iteration converts one
StackVal::Reg to Spilled. Honest remaining bound: the i64 spill-slot pool
("spill-slot pool exhausted"), deliberately NOT retried.

EVIDENCE
- scripts/repro/high_pressure_i64.wat: 4 simultaneously-live i64 consts +
  4 pinned i32 params. On main: skipped with the exact pair Err. On this
  branch: compiles; high_pressure_i64_differential.py (unicorn vs wasmtime,
  i64 result in r0:r1, large/negative vectors) 6/6 ORACLE PASS.
- Bit-identity vs origin/main (cortex-m4, per differential headers):
  control_step 8248…32f6, div_const cada…45ede, flight_seam_flat 03ce…8579,
  high_pressure_i32 355c…e6c2 — all pairwise sha256-identical; all three
  frozen differentials ORACLE PASS on the branch.
- Tests: 5 new (pair victim spill, single victims, pinned-blockers Err
  preserved verbatim, spilled-pair reload on pop, end-to-end 3-pass ladder);
  synth-synthesis 382 lib tests green; workspace 1468 passed / 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.79167% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-backend/src/arm_backend.rs 80.00% 7 Missing ⚠️
crates/synth-synthesis/src/instruction_selector.rs 98.08% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@avrabe
avrabe merged commit 84e7aaf into main Jun 11, 2026
14 checks passed
@avrabe
avrabe deleted the feat/vcr-ra-pair-spill branch June 11, 2026 13:35
avrabe added a commit that referenced this pull request Jul 3, 2026
…emainder (#587, #242) (#589)

Extend the SYNTH_SPILL_ON_EXHAUST pre-step (#580) from i32 singles to i64
register PAIRS on the optimized path. When an even-aligned pair allocation
would fail, the pre-step evicts either (a) a coherent live PAIR or (b) two
independent singles freeing a legal even pair — choosing the candidate whose
displaced values have the farthest soonest-next-use (pair-aware Belady),
deterministic tie-breaks by fixed candidate order. Pair victims get
8-byte-aligned slot couples (lo at off, hi at off+4 — the #171/#325
direct-path convention) and reload as coherent pairs into any legal even
pair at next use; a spilled final i64 result reloads straight into R0:R1 in
the epilogue. If no sound eviction exists the function falls through to the
existing #496 decline — loud skip, never wrong code.

Scope stays honest: only the modeled i64 subset is admitted (const/load-param/
add/sub/and/or/xor/mul, comparisons+eqz, i32<->i64 conversions); shifts/
rotates (encoders clobber the amount pair in place), div/rem, clz/ctz/popcnt,
non-param i64 locals, and i64+globals/memory.size mixing (R9/R10 conventions
collide with the (R8,R9)/(R10,R11) candidates) keep declining. The candidate
list is shared with alloc_i64_pair via SPILL_I64_PAIR_CANDIDATES so evictor
and allocator cannot drift.

Red -> green: scripts/repro/i64_pair_exhaust_587.wat (5 simultaneously-live
i64s over 4 candidate pairs) declined on BOTH flag states at v0.24.0; now
flag-on it stays on the optimized path (no recovery rung) and matches
wasmtime under unicorn on all 8 vectors (flag-off decline pinned + still
matches via the direct selector). Flag-off ELF is byte-identical to the
pre-change baseline; frozen_codegen_bytes (incl. escape hatches), const_cse
golden, spill_on_exhaust_242 (i32) and i64_param_518 differentials all pass.

Flag stays default-OFF pending gale silicon, per the #587 triage.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant