feat(selector): i64 stack params + pair spill-slot growth — direct-path completeness (#503, #587) - #605
Merged
Merged
Conversation
…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 Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
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.
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_wrinscripts/repro/i64_stack_param_503.watall 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(falconfunc_58/func_163/func_164class).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_paramsis now width-aware(offset, is_i64);LocalGet/LocalSet/LocalTeeread/write wide slots viaI64Ldr/I64Str [sp, #frame+24+nsaa](12-bit guard coversoff+4for the hi half).param_slotsbacks only register-resident params. Wide stack params in has-call functions lower too (the caller's frame trivially survives theBL); the #518 decline is narrowed to REGISTER-resident i64 params in frame-backing functions — still a loud skip, still cited byd_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
p3of(i64 i32 i32 i32)is stack-passed — the oldindex_to_regfallback read it from R3 (= p2) and compiled without warning (s_narin 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 inarm_backend.Green:
scripts/repro/i64_stack_param_503_differential.py— 8 shapes × 2 paths (direct--relocatableincl. THM_CALL patching, and default) all match wasmtime under unicorn, exit 0.Still declined (honest, tracked): outgoing i64 call args past R3 (
pop_call_argsrefusal, 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 falconfunc_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'sfunc_60/func_73bullet.)Fix: the pool is growable —
SpillStateis Vec-backed,compute_local_layoutreservesi64_spill_slots × 8bytes, and a new pool-grow recovery retry inarm_backend::select_directreruns 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 (newsynth_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 andpromotion_never_causes_compile_failure_474caught it changing a compiles-today function's bytes; final ordering keeps it green.Green:
scripts/repro/i64_spill_pool_587_differential.py—hp20compiles viarung=pool-grow result=okand 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_001etc.) 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.max_depth_bound.i64_completeness_503_587.rs(no skips + pool-grow rung non-vacuity).test_359_i64_stack_param_errs→test_503_i64_stack_param_lowers;i64_param_518_differential.pymovesd_past_r3from 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.i64-completeness-503-587-oraclejob runs both differentials.Refs #587 (direct-path remainder), #518, #242 (VCR-RA), #494.
🤖 Generated with Claude Code