Repository navigation
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
Description
Activity
[issue-hunt loop] Fixed in PR #475 — promotion never causes a compile failure anymore.
Root cause confirmed exactly as you diagnosed: promotion pins r4-r8 for the locals, halving the temp pool; on a dense function the allocator can't recover.
Fix: the exhaustion-recovery ladder is now parameterized on promotion. It runs with promotion first (so every function that compiles today is bit-identical — frozen byte gate green), and only if it still ends in register exhaustion falls back to the promotion-off ladder automatically — exactly your
SYNTH_NO_LOCAL_PROMOTE=1workaround, now automatic. Keys onregister exhaustiongenerally, so it covers both the single-register and the i64-spill-pool exhaustion classes promotion can trigger.Verified on a generic repro (
promotion_exhaustion_fallback.wat): fails without the fix (i64 spill-slot pool exhausted), compiles with it,.textbyte-identical to the promotion-off build. New CI regression test guards it. Frozen gate stays green.I couldn't grab
control_step_packedto repro your exactall allocatable registers are live on the stackstring (same class — both are promotion-induced exhaustion the fallback now catches). If you can re-runcontrol_step_packedon the PR #475 branch / G474RE once it lands, that closes the loop on the real function. Headed for a v0.15.1 bug-fix release on merge.— issue-hunt loop (avrabe automation), not a maintainer decision.
- added 4 commits that reference this issue
on Jun 24, 2026 Still reproduces on v0.15.0 — control_step_packed register-exhausts (please reconsider closure)
This is marked closed, but the default-on local promotion still fails on the dense
control_step_packedwith synth 0.15.0. Compiling withoutSYNTH_NO_LOCAL_PROMOTE:$ synth compile ec.opt.wasm --target cortex-m3 --all-exports --relocatable \ --native-pointer-abi --shadow-stack-size 8192 -o cs.o INFO Compiling function 'control_step_packed' via backend 'arm'... warning: skipping function 'control_step_packed': backend 'arm' failed: compilation failed: instruction selection failed: Synthesis failed: register exhaustion: all allocatable registers are live on the stack — function too complex for current register allocator warning: 1 of 1 functions were skipped (not in output): control_step_packed Error: no functions compiled successfully (1 skipped) — nothing to emitWith
SYNTH_NO_LOCAL_PROMOTE=1it compiles clean (568 B, was 580 B on 0.12.0 — it still benefits from the other three levers). So gale'scontrol_step-cm3/cm4.oships with the flag set (pulseengine/gale#112), i.e. the densest real function in the body can't take the v0.14 local-promotion lever.Repro source: gale
benches/engine_control/{src/control.c,src/tables.c}+wasm-dissolve/shim.c→clang --target=wasm32→wasm-ld --export=control_step_packed→loom optimize --passes inline→ the synth compile above. The cost-gate still needs reg-pressure awareness (back off promotion when the function is already near the allocatable-register ceiling) rather than promoting unconditionally. Happy to attach the exactec.opt.wasmif useful.[issue-hunt loop] The fix is in v0.15.1 — you tested v0.15.0 (pre-fix). Please re-test on v0.15.1; it should compile
control_step_packedwith promotion default-on, no flag.Your exact error —
register exhaustion: all allocatable registers are live on the stack— is precisely the class the v0.15.1 fix catches. The exhaustion-recovery ladder is now parameterized on promotion: it runs with promotion first (so every function that compiles today stays bit-identical), and only if it still ends in register exhaustion falls back to the promotion-off ladder automatically — exactly yourSYNTH_NO_LOCAL_PROMOTE=1workaround, now automatic. The fallback keys onregister exhaustiongenerally, which includes your string verbatim (arm_backend.rs:e.to_string().contains("register exhaustion")).So on v0.15.1
control_step_packedshould: compile (no skip), via the frame-slot lowering for its locals (the densest function loses only the local-promotion lever, keeping the other three — same as your manual-flag result), no env var needed. That restores compilability, which is the regression this issue is.Two asks to close the loop:
- Re-confirm on v0.15.1 (it's the Latest release). If it still skips, reopen — that would be a real miss and I'll chase it.
- Please do attach
ec.opt.wasm— I couldn't reproduce your exactall allocatable registers are live on the stacksub-class with synthetic fixtures (mine hit the i64-spill-pool sub-class; both are caught, but I want your real function as a committed regression fixture + to verify the rescue on it directly).
On the deeper ask — your "back off promotion when near the ceiling rather than promote unconditionally": v0.15.1 does the coarse version (drop promotion entirely on exhaustion). Partial promotion (promote what fits, frame-slot the rest) is the finer cost-gate; tracked under #242 as a follow-on, not a blocker for the regression.
— issue-hunt loop (avrabe automation), not a maintainer decision.
- added a commit that references this issue
on Jun 25, 2026 Verified FIXED on v0.15.1 (PR #475).
control_step_packednow compiles clean withoutSYNTH_NO_LOCAL_PROMOTE— no register exhaustion, 568 B (synth compile --target cortex-m3 --native-pointer-abi --shadow-stack-size 8192). The recovery-ladder approach (promote, then gracefully fall back instead of failing) works on gale's densest real function. gale can now drop the SYNTH_NO_LOCAL_PROMOTE=1 workaround. Thank you for the fast turnaround.- added a commit that references this issue
on Jul 3, 2026
v0.14.0 default-on local promotion register-exhausts on
control_step(compile FAILURE on a denser real function)The local-promotion lever (v0.14.0 default-on, #462) is a net win on
gust_mix(2 locals — confirmed −14% on G474RE silicon). But on a denser real function it turns a working compile into a hard failure.Repro: gale's
control_step_packed(the engine_control algorithm — table lookups + integer corrections + the spark/fuel packing; ~5 i32 locals, 14 loads/8 stores), dissolvedwasm→loom→synth --target cortex-m3 --native-pointer-abi --shadow-stack-size 8192 --all-exports --relocatable:It compiled on v0.12.0 (pre-promotion). So default-on promotion regressed it from compilable to skipped. The cause is exactly what you flagged when handing me the lever: promotion claims callee-saved regs for the locals, and on a function with enough live values the allocator then has nowhere to spill → exhaustion. gust_mix (2 locals) had headroom; control_step doesn't.
Ask: make the promotion eligibility register-pressure-aware — decline (or partially promote) when promoting would push peak pressure past what the allocator can color, rather than failing instruction selection. The shadow allocator / peak-pressure analysis you already have (
function_peak_pressure) is the natural gate: promote up toavailable_regs − reserved, frame-slot the rest. A compile failure on a real function is worse than the spill it was avoiding.Workaround in the field:
SYNTH_NO_LOCAL_PROMOTE=1(or the escape-hatch env) restores the working lowering. gale's gust_control demonstrator (engine_control driven on the kiln stack) ships built with it for now.Refs: synth#428 (lever tracker); the lever is #462/#458. I can re-check on the G474RE/ESP32-C3 once the cost-gate lands.