Skip to content

feat(#851): aarch64 full control flow — if/else, loop, return#857

Open
avrabe wants to merge 5 commits into
mainfrom
feat/aarch64-controlflow-851
Open

feat(#851): aarch64 full control flow — if/else, loop, return#857
avrabe wants to merge 5 commits into
mainfrom
feat/aarch64-controlflow-851

Conversation

@avrabe

@avrabe avrabe commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Extends the -b aarch64 backend's void-block-only #538 control flow
(block/br/br_if) with the remaining structured constructs, part of lane
L4 of the v0.51 hub. Closes the last major control-flow gap for the host-native
integer subset (#851).

Lowered (all execution-verified bit-identical vs wasmtime):

  • if/elsecbz cond → else/end, unconditional b over the else arm,
    join at end (void (0,0) only).
  • loop — backward branch target: a br/br_if to a loop frame resolves a
    negative offset eagerly at the loop header (void (0,0) only).
  • return — early epilogue (funnel top → x0/d0, restore SP, ret).

Design

  • Kind-tagged control frames. enum Kind { Block, Loop { entry }, If { else_fixup } }.
    Branch resolution dispatches on the target frame's kind — Block/If are
    forward (recorded in pending, patched at End), Loop is backward (resolved
    on emission). This is what lets loop back-edges and forward block exits coexist.
  • patch_branch helper centralizes the b/cbnz/cbz re-encode so the new
    cbz can't be mis-patched as a cbnz; it is fallible and range-checks every
    displacement.
  • Reachability flag applies WASM's stack-polymorphism rule after
    br/return/unreachable: the value stack is truncated unconditionally
    (fixes the model) but the fall-through height debug_assert is gated on
    reachability.
  • block_arity ordinal is now consumed by Loop and If (one entry each),
    keeping the arity-table lookup aligned.
  • Range guards (check_imm26/check_imm19): displacements exceeding the A64
    field width LOUD-DECLINE rather than wrap silently.

Honest frontier (loud-decline, never silent)

  • Value-producing if/loop (arity ≠ (0,0) — both arms would need the result
    in one register / the value stack live across the back-edge).
  • br_table (catch-all).
  • Over-range branch displacements.

Gate — RED-first execution differential

scripts/repro/aarch64_ctrlflow_851_differential.py (28 cases), native arm64
(MAP_JIT fork) + unicorn vs wasmtime:

  • RED before: all 7 functions declined (If unsupported, loop declined by name).
  • GREEN after: 28/28 bit-identical — if/else (both arms), counting + countdown +
    do-while loops (the do-while drives the backward conditional cbnz-to-header
    path), early-return, return-inside-loop. Both branch edges + a trap edge
    exercised; non-vacuity-guarded (every function + both edges must run).

Verification

  • Backend unit tests 70/70 (new: void-if cbz skip, if/else both-edge patch, loop
    back-edge negative offset, early-return epilogue).
  • scripts/repro/aarch64_matrix.sh PASS (32 ops accepted, 86 native checks).
  • Pre-existing aarch64_cf_538 / locals_851 / mem_851 differentials still GREEN.
  • frozen_codegen_bytes 10/10 untouched.
  • cargo fmt --check + cargo clippy --workspace --all-targets -D warnings clean.

Coordinator note

The new differential needs wiring into .github/workflows/ci.yml alongside
aarch64_cf_538_differential.py / aarch64_mem_851_differential.py at assembly
(this lane does not edit repo config).

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

avrabe and others added 5 commits July 23, 2026 20:49
if/else, loop back-edge, return execution differential — native arm64
(MAP_JIT fork) + unicorn vs wasmtime. RED before lowering: all 7 functions
decline (If unsupported, loop declined by name).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Extends the void-block-only #538 control flow (block/br/br_if) with:
- if/else: kind-tagged frame; cbz cond -> else/end, unconditional b over the
  else arm, join at end. Void (0,0) only — value-if loud-declines.
- loop: backward-branch target; br/br_if to a loop frame resolves a NEGATIVE
  offset eagerly (loop header known at push). Void (0,0) only.
- return: early epilogue (funnel top -> x0/d0, restore SP, ret).

Branch dispatch is on the TARGET frame's kind (Block/If = forward pending,
patched at End; Loop = backward, resolved now), so loop back-edges and forward
block exits coexist. patch_branch() centralizes b/cbnz/cbz re-encode so the new
cbz can't be mis-patched as cbnz.

Reachability flag: code after br/return/unreachable is stack-polymorphic
(WASM); truncate the value stack unconditionally but gate the fall-through
height debug_assert on reachability.

block_arity ordinal now consumed by Loop and If (one entry each), keeping the
arity-table lookup aligned. br_table still loud-declines (catch-all).

Execution gate: aarch64_ctrlflow_851_differential.py — 25 cases, both branch
edges + loop trip counts, native arm64 (MAP_JIT fork) + unicorn vs wasmtime,
all GREEN (was RED: all 7 fns declined).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Backend unit tests for the new lowerings: void-if cbz skip, void-if/else
both-edge patch, loop back-edge negative offset, early-return epilogue. Update
the old decline test to br_table-and-typed-loop/if only (loop/if now lowered
for the void shape). Fix the countdown fixture to copy the param into a
non-param local (param writes loud-decline). 70/70 backend tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Address advisor tightening:
- check_imm26/check_imm19 guard every branch displacement (eager backward
  Br/BrIf-to-loop + patched forward b/cbz/cbnz) against the A64 field width;
  over-range LOUD-DECLINES instead of wrapping silently (No silent miscompile).
  patch_branch is now fallible and propagated at all three call sites.
- do_while_count fixture: br_if BACK to the loop header exercises the backward
  CONDITIONAL branch (cbnz-to-header) that br 0 + br_if 1 loops never hit.
  Differential now 28 cases, all GREEN.

fmt clean, workspace clippy -D warnings clean, 70/70 backend tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.02326% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-backend-aarch64/src/selector.rs 93.02% 12 Missing ⚠️

📢 Thoughts on this report? Let us know!

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