Skip to content

feat(selector): i64 stack params + pair spill-slot growth — direct-path completeness (#503, #587) - #605

Merged
avrabe merged 1 commit into
mainfrom
feat/503-587-direct-i64-completeness
Jul 3, 2026
Merged

feat(selector): i64 stack params + pair spill-slot growth — direct-path completeness (#503, #587)#605
avrabe merged 1 commit into
mainfrom
feat/503-587-direct-i64-completeness

Conversation

@avrabe

@avrabe avrabe commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Closes the two honest-skip classes that kept falcon's i64-heavy helpers out of the ELF on the direct/ARM path. Per-class red→green below; frozen anchors bit-identical.

Class 1 — #503-i64: 64-bit params AAPCS-passed on the stack (Closes #503)

Red (pinned): s_mix/s_align/s_p3/s_call/s_wr in scripts/repro/i64_stack_param_503.wat all loud-skipped on v0.28.0 main with #503: function has N params including a 64-bit (i64/f64) param… / #518/#503: an i64/f64 param is AAPCS-passed past R3 (falcon func_58/func_163/func_164 class).

Fix: new aapcs_param_layout — a full NCRN+NSAA walk over declared widths. Every param is either register-resident (even-aligned pair for wide) or gets a caller-frame NSAA offset (8-byte-aligned for wide). compute_local_layout.incoming_params is now width-aware (offset, is_i64); LocalGet/LocalSet/LocalTee read/write wide slots via I64Ldr/I64Str [sp, #frame+24+nsaa] (12-bit guard covers off+4 for the hi half). param_slots backs only register-resident params. Wide stack params in has-call functions lower too (the caller's frame trivially survives the BL); the #518 decline is narrowed to REGISTER-resident i64 params in frame-backing functions — still a loud skip, still cited by d_call.

Found along the way (silent miscompile, now fixed on BOTH paths): per AAPCS C.5 there is no register back-fill after a stack spill, so p3 of (i64 i32 i32 i32) is stack-passed — the old index_to_reg fallback read it from R3 (= p2) and compiled without warning (s_nar in the fixture). The optimized path had the same width-naive homing (its #518 decline only fires on i64-param reads), so wide-param signatures are now routed to the direct selector in arm_backend.

Green: scripts/repro/i64_stack_param_503_differential.py — 8 shapes × 2 paths (direct --relocatable incl. THM_CALL patching, and default) all match wasmtime under unicorn, exit 0.

Still declined (honest, tracked): outgoing i64 call args past R3 (pop_call_args refusal, cites #503) and register-resident i64 params + call (#518) — neither is a falcon signature case.

Class 2 — #587 direct-path remainder: i64 spill-slot pool exhaustion

Red (pinned): scripts/repro/i64_spill_pool_587.wat (20 simultaneously-live i64 consts, ~16 concurrent pair spills vs the fixed 8-slot pool) loud-skipped with exactly falcon func_60/func_73's message: register exhaustion: i64 spill-slot pool exhausted — function too complex for current register allocator. (#587 itself is closed; this is its direct-path remainder, per the issue's func_60/func_73 bullet.)

Fix: the pool is growable — SpillState is Vec-backed, compute_local_layout reserves i64_spill_slots × 8 bytes, and a new pool-grow recovery retry in arm_backend::select_direct reruns the entire existing sequence (base → spill → param-backing rungs, promotion-on then the #474 promotion-off fallback) with the pool sized from a conservative operand-stack-depth bound (new synth_core::wasm_stack_check::max_depth_bound, +4 transient slots, clamped to 120 = 960 B, 12-bit-friendly). Deliberately last resort: any function that compiled yesterday is produced by exactly yesterday's path — first draft had the rung inside the promote-on ladder and promotion_never_causes_compile_failure_474 caught it changing a compiles-today function's bytes; final ordering keeps it green.

Green: scripts/repro/i64_spill_pool_587_differential.pyhp20 compiles via rung=pool-grow result=ok and matches wasmtime on 4 args × 2 paths, exit 0.

Gates

  • cargo test -p synth-synthesis -p synth-cli — all green (46 test binaries, 0 failures), including the frozen byte gates (frozen_fixtures_text_is_bit_identical_oracle_001 etc.) and v0.14.0 default-on local promotion register-exhausts on denser functions (control_step) — compile FAILURE, was fine on 0.12.0; cost-gate needs reg-pressure awareness #474 bit-identity.
  • cargo fmt --check + cargo clippy --workspace --exclude synth-verify --all-targets -- -D warnings — clean.
  • New unit tests: AAPCS NSAA offset matrix, i64-stack-param lowers, narrow-after-wide reads stack not R3, wide-stack+call lowers / wide-reg+call still declines, pool-grow rescues 20-live-i64, max_depth_bound.
  • New CLI integration test i64_completeness_503_587.rs (no skips + pool-grow rung non-vacuity).
  • Updated pinned artifacts: test_359_i64_stack_param_errstest_503_i64_stack_param_lowers; i64_param_518_differential.py moves d_past_r3 from the decline contract to emitted+executes-correctly (full run exit 0); adjacent oracles (stack_args_503, spill_on_exhaust_242, spill_rung_581, high_pressure_i64, i64_load_store_372) all exit 0.
  • CI: new isolated i64-completeness-503-587-oracle job runs both differentials.

Refs #587 (direct-path remainder), #518, #242 (VCR-RA), #494.

🤖 Generated with Claude Code

…th completeness (#503, #587)

Closes the two remaining honest-skip classes in the direct selector's
i64 handling:

#503-i64 — 64-bit params AAPCS-passed on the STACK. New
aapcs_param_layout walks NCRN+NSAA with declared widths: a wide param
past R3 (or even-align-spilled, e.g. p3 of (i32 i32 i32 i64)) gets an
8-byte-aligned caller-frame slot read/written via I64Ldr/I64Str at
[sp, frame+24+nsaa]; per AAPCS C.5 a narrow param AFTER a stack spill
is itself stack-passed. That C.5 rule also fixes a latent SILENT
miscompile found along the way: p3 of (i64 i32 i32 i32) was read from
R3 (= p2) by the index_to_reg fallback — on BOTH paths (the optimized
path now routes wide-param signatures to the direct selector; its own
homing is width-naive and its #518 decline only caught i64-param
READS). Wide STACK params in has-call functions lower too (the caller
frame survives the BL); the #518 decline is narrowed to REGISTER-
resident i64 params in frame-backing functions, which remain the one
loud skip. param_slots now backs only register-resident params.

#587 — i64 spill-slot pool exhaustion (falcon func_60/func_73). The
pool (8 slots) is growable: a new pool-grow recovery retry reruns the
ENTIRE existing ladder (every rung + the #474 promotion-off fallback —
deliberately last, so anything that compiled yesterday is produced by
exactly yesterday's path) with the pool sized from a conservative
operand-stack-depth bound (new synth_core::wasm_stack_check::
max_depth_bound; clamped to 120 slots). SpillState is Vec-backed;
compute_local_layout reserves the matching area.

Freeze: frozen byte gates + promotion_never_causes_compile_failure_474
bit-identical (pool-grow and the wide-param machinery only fire on
previously-skipped/miscompiled shapes; all-i32 signatures produce the
exact legacy offsets by construction).

Oracles (wasmtime ground truth vs unicorn, both paths, CI-wired):
  scripts/repro/i64_stack_param_503_differential.py  (7 shapes + control)
  scripts/repro/i64_spill_pool_587_differential.py   (20-live-i64 fold)
i64_param_518_differential.py updated: d_past_r3 moved from the
decline contract to emitted+executes-correctly.

Closes #503

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

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.42647% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-synthesis/src/instruction_selector.rs 96.50% 7 Missing ⚠️

📢 Thoughts on this report? Let us know!

@avrabe
avrabe merged commit 2832364 into main Jul 3, 2026
28 checks passed
@avrabe
avrabe deleted the feat/503-587-direct-i64-completeness branch July 3, 2026 11:17
avrabe added a commit that referenced this pull request Jul 3, 2026
…ranch-geometry hardening (#608)

Hub wave 2: #604 (const-CSE flip, hazard retired, CF-sound), #605 (i64 stack
params + pool growth, closes #503), #607 (five passes geometry-hardened,
closes #606). Pin sweep + lock + CHANGELOG.

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.

arm: functions needing the AAPCS stack-arg path (>8 scalar params, or any 64-bit stack param) are skipped, not lowered

1 participant