Skip to content

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

@avrabe

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), dissolved wasm→loom→synth --target cortex-m3 --native-pointer-abi --shadow-stack-size 8192 --all-exports --relocatable:

synth 0.14.0 (default, promotion on):
  warning: skipping function 'control_step_packed': backend 'arm' failed:
  instruction selection failed: Synthesis failed: register exhaustion:
  all allocatable registers are live on the stack — function too complex
  for current register allocator
  Error: no functions compiled successfully — nothing to emit

synth 0.14.0 + SYNTH_NO_LOCAL_PROMOTE=1:
  Compiled 1 functions  (.text 534 B)   ← compiles fine

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 to available_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.

Activity

  1. avrabe commented on Jun 24, 2026

    @avrabe
    ContributorAuthor

    [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=1 workaround, now automatic. Keys on register exhaustion generally, 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, .text byte-identical to the promotion-off build. New CI regression test guards it. Frozen gate stays green.

    I couldn't grab control_step_packed to repro your exact all allocatable registers are live on the stack string (same class — both are promotion-induced exhaustion the fallback now catches). If you can re-run control_step_packed on 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.

  2. avrabe commented on Jun 25, 2026

    @avrabe
    ContributorAuthor

    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_packed with synth 0.15.0. Compiling without SYNTH_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 emit
    

    With SYNTH_NO_LOCAL_PROMOTE=1 it compiles clean (568 B, was 580 B on 0.12.0 — it still benefits from the other three levers). So gale's control_step-cm3/cm4.o ships 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 exact ec.opt.wasm if useful.

  3. avrabe commented on Jun 25, 2026

    @avrabe
    ContributorAuthor

    [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_packed with 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 your SYNTH_NO_LOCAL_PROMOTE=1 workaround, now automatic. The fallback keys on register exhaustion generally, which includes your string verbatim (arm_backend.rs: e.to_string().contains("register exhaustion")).

    So on v0.15.1 control_step_packed should: 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:

    1. 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.
    2. Please do attach ec.opt.wasm — I couldn't reproduce your exact all allocatable registers are live on the stack sub-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.

  4. avrabe commented on Jun 25, 2026

    @avrabe
    ContributorAuthor

    Verified FIXED on v0.15.1 (PR #475). control_step_packed now compiles clean without SYNTH_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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions