jit: decline wasm bridges with CallMayForce re-entry (real fix for the #404 workaround)#407
Conversation
…gate A wasm-compiled guard side-trace containing a may-force residual call reconstructs the interpreter value stack from its inputargs before the call; a float-const dividend materialises as NULL there, so the re-entered interpreter raises `unsupported operand type(s) for /`. Native blackholes that deopt instead. Decline such bridges in the wasm backend so the deopt blackholes, matching native. With the deopt handled this way, the #366 non-branch pc carry resumes correctly on wasm, so remove the wasm-only gate in `m366_nonbranch_pc_enabled` (added in #404) and its stale doc-comment.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughThe wasm JIT backend now declines compiling bridges containing a CallMayForce opcode, returning BackendError::Unsupported so execution routes through the blackhole interpreter instead. Separately, m366_nonbranch_pc_enabled() removes its wasm32-specific hard-disable and instead derives its cached state from the PYRE_M366_NONBRANCH_PC environment variable. ChangesWasm Bridge CallMayForce Guard
m366 Nonbranch PC Flag Fix
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🤖 Codex parity reviewStatic analysis of this diff vs the local RPython/PyPy sources (commit cd24acc). 1. Regressions to PyPy parity introduced by this patch
2. Other mismatches introduced by this patchNone. 3. Pre-existing mismatches (already present before this patch)
4. Structural adaptations
|
|
| config | bool_arithmetic | is_op | string_ops | suite total |
|---|---|---|---|---|
A = origin/main #404 (m366 OFF, no decline) |
2.73s | 0.20s | 1.25s | 125.25s |
| B = this PR (m366 ON + decline) | 15.84s | 1.81s | 3.72s | 139.19s (+11%) |
| C = m366 OFF + decline | 16.80s | 2.06s | 3.81s | 144.72s |
Since C ≈ B and both ≫ A, the regression is caused by the bridge decline, not m366 (m366 ON/OFF on wasm is noise-level). bool_arithmetic/is_op/string_ops compile legitimate, fast CallMayForce bridges on wasm; declining all of them kills those speedups. Only the float-truediv deopt bridge is actually broken.
So #404's targeted m366-off was correct and had zero perf cost, while this blanket decline is a net regression.
Real root cause / proper fix direction: the wasm bridge inputarg/resume path materialises the float const 1.5 as NULL. Lead: emit_resolve bakes constants.get(&opref.raw()).unwrap_or(0) — a float ConstPtr missing from the bridge resume constants table falls back to 0 (NULL). Fixing float-const carriage in the bridge resume data would keep all bridges enabled (perf preserved) and correct — no decline, no m366 gate needed.
Holding this draft pending a decision: (1) real codegen fix [recommended], (2) close and keep #404, or (3) narrow the decline to only the float-const-rebox shape.
…on) (#416) #407 declined every CallMayForce bridge on wasm, which also dropped the non-raising loop-closing bridges wasm relies on for speed (bool_arithmetic ~6x slower, ~+11% on the synthetic suite). Only exception-resume bridges are actually broken: a bridge containing `GuardException` resumes into the exception handler by re-entering the interpreter at the raising bytecode, reading the pre-call operand stack. Constant stack entries (e.g. a float dividend) live only in the guard's `rd_consts` resume data, not in a spilled frame slot, and the compiled bridge — reconstructing state from inputargs alone — cannot see `rd_consts`, so the constant materialises as NULL and the re-entered interpreter runs e.g. `truediv(NULL, int)`. Narrow the decline from `is_call_may_force()` to `GuardException`: the broken shape blackholes (rematerialising constants from `rd_consts`, the native path) while `GuardNoException` loop-closing bridges keep their compiled fast path. wasm synthetic suite 173/173 (float_div correct), no CallMayForce-bridge regression, and faster overall than both #407 and the #404 workaround.
Summary
Real fix for the wasm
float_div_zero_caught_loopfailure that #404 worked around by gating the #366 non-branch direct-pc carry off on wasm.Root cause. Under the m366 carry, the checked-float-truediv deopt (
float_eq(rhs,0.0) -> guard_false, every 5th iteration) gets a guard side-trace (bridge) compiled for it on wasm only. That bridge doesNewWithVtable+SetfieldGc+CallMayForceR(...1.5-float-const..., v80, 5)— it re-enters the plain interpreter through a may-force residual call. On wasm the reconstructed interpreter value stack has the1.5dividend as NULL, so the re-entered interpreter runstruediv(NULL, int)→TypeError: unsupported operand type(s) for /: '' and 'int'. Native never compiles this bridge — it blackholes the deopt and prints37500.0.The earlier "wasm liveness twin ≠ raw
-live-decode" (#404) and a later "wasm32 32-bit object-layout corruption" hypothesis were both wrong; the latter was a stale-wasm-module measurement artifact. The blackhole path runs byte-identically on wasm and native — the divergence is native-blackhole vs wasm-bridge.Fix
majit-backend-wasmcompile_bridge— decline any bridge whose ops contain a may-force call, so the deopt routes back through the blackhole interpreter, matching native.jitcode_dispatch.rs— with the deopt handled, the m366 carry resumes correctly on wasm, so the wasm-only gate inm366_nonbranch_pc_enabled(added in jit: disable the #366 non-branch direct-pc carry on wasm #404) and its stale doc-comment are removed. m366 is now ON on all backends (parity).dynasm/cranelift codegen is unaffected by construction: the lib.rs change is wasm-backend-only, and the removed jitcode gate branch was never taken on native.
Verification
wasm synth/float_div_zero_caught_loop→37500.0Summary by CodeRabbit
Bug Fixes
Documentation