Skip to content

Put the AArch64 branch-range fallback on the path the JIT compiles through (#486) - #493

Merged
Milky2018 merged 1 commit into
mainfrom
milky/iss-402-jit-branch-fallback
Aug 6, 2026
Merged

Put the AArch64 branch-range fallback on the path the JIT compiles through (#486)#493
Milky2018 merged 1 commit into
mainfrom
milky/iss-402-jit-branch-fallback

Conversation

@Milky2018

Copy link
Copy Markdown
Owner

GitHub #486's reported failure still reproduces on main:

python3 scripts/gen_wide_branch_wat.py 33000 /tmp/wide33k.wat
./wasmoon run /tmp/wide33k.wat --invoke wide --arg 0
AArch64CompilationFailed(cause=EmissionFailed(cause=BranchOutOfRange(offset=1115408, bits=19)))

33,000 memory read-modify-writes inside an if compile to about 1.12 MB, which puts the conditional branch past imm19's ±1 MB reach.

Root cause

There are two ways to turn AArch64 VCode into machine code, and the fallback added for #486 is on the one the JIT does not use.

Entry point Reached from Fallback
aarch64_target.emit diagnose_aarch64_targetwasmoon explore yes
aarch64_target.compile compile_aarch64_target → the JIT and wasmoon run no

emit wrapped emit_verified in a compact-then-wide retry. compile called emit_verified directly, so a BranchOutOfRange from the compact pass propagated instead of being retried at the wide width.

That the two disagree is also what hid it: the natural way to investigate is wasmoon explore, which takes the working path. explore compiled the failing module without complaint while run failed on it — that inconsistency is what located the seam.

The name is what made it easy to get wrong. emit_verified reads like the checked, proper emit, so it is the obvious thing for a pipeline stage to call, while in fact being one attempt at a fixed branch width.

This is the third time the same shape has happened on this issue: #487 widened only the fixups from CodeBuffer::emit_branch, #490 routed emit_edges.mbt's direct patches through the fixup path, and this is the entry point above both of them.

Change

  • emit_verifiedemit_single_pass, documented as one attempt at a fixed width.
  • The retry moves into emit_with_branch_fallback; emit and compile both call it. emit keeps the verify_frame it owes its callers; compile still skips it, since plan_frame_verified established that invariant.
  • run forwards the Failed stage message. The compiler already computed why it gave up; the CLI passed a no-op progress callback and logged the bare string JIT compilation failed. The reporter of Wasm frontend threads every local through each control merge, causing AArch64 code-size explosion #486 saw a detailed message only because they embed wasmoon as a library.

On the missing regression test

One was written and withdrawn on cost. A branch only outgrows imm19 across a megabyte of code, and at four bytes per instruction that is upward of 262,144 instructions however the fixture is built — the span is the code size, so there is no small-but-far version. Driving that through compile as chained adds, with verify_allocation=false since allocation checking dominates at that size (ISS-371), took the native suite from 5:10 to 16:32. Eleven minutes on both CI platforms for every PR is not worth one test.

scripts/gen_wide_branch_wat.py carries the reproduction instead, with sizes and expected results in its docstring — the same arrangement long_branch_wbtest.mbt already documents for the encoding half.

Verification

  • The 1.12 MB module and a 1.5 MB one (comparable to the reported 1,471,440-byte offset) both compile and agree with the interpreter: 512 and 701.
  • Workspace 2634/2634 native, 1028/1028 wasm-gc, moon check clean from a cold moon clean.
  • WAST corpus 258/258 files, 62563 assertions, both interpreter and JIT.

What this does not close on #486

)

GitHub #486's reported failure still reproduces on main. 33,000 memory
read-modify-writes inside an `if` compile to about 1.12MB, which puts the
conditional branch past imm19, and `wasmoon run` fails with the same error
the reporter filed:

  AArch64CompilationFailed(cause=EmissionFailed(
    cause=BranchOutOfRange(offset=1115408, bits=19)))

There are two ways to turn AArch64 VCode into machine code and the
fallback was on the one the JIT does not use. `emit` wrapped
`emit_verified` in a compact-then-wide retry and is reached only from
`diagnose_aarch64_target`, i.e. `wasmoon explore`. `compile` — what
`compile_aarch64_target` and therefore the JIT calls — called
`emit_verified` directly, so an overflow from the compact pass propagated
instead of being retried at the wide width. That the two disagree is also
what hid it: the natural way to investigate takes the working path, and
`explore` compiled the failing module without complaint.

The name is what made it easy to get wrong. `emit_verified` reads like
the checked, proper emit, so it is the obvious thing for a pipeline stage
to call, while being one attempt at a fixed branch width. It is now
`emit_single_pass`, the retry is `emit_with_branch_fallback`, and both
`emit` and `compile` go through that. `emit` keeps the `verify_frame` it
owes its callers; `compile` still skips it, since `plan_frame_verified`
established that invariant already.

This is the third time the same shape has happened here: #487 widened
only the fixups from `CodeBuffer::emit_branch`, #490 routed the direct
patches in `emit_edges.mbt` through the fixup path, and this is the entry
point above both of them.

`run` also discarded the reason. The compiler hands it to the `Failed`
stage event, but the CLI passed a no-op callback and logged the bare
string "JIT compilation failed" — the reporter saw a detailed message
only because they embed wasmoon as a library. It now forwards the message.

An automated regression was written and withdrawn on cost: a branch only
outgrows imm19 across a megabyte of code, so no fixture can be both large
enough and cheap, and driving one through `compile` took the native suite
from 5:10 to 16:32. scripts/gen_wide_branch_wat.py carries the
reproduction instead, as long_branch_wbtest.mbt already does for the
encoding half.

Verified: the 1.12MB and 1.5MB modules now compile and agree with the
interpreter (512 and 701); workspace 2634/2634 native and 1028/1028
wasm-gc; corpus 258/258 files and 62563 assertions in both modes.
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.

1 participant