feat(#851): aarch64 non-param locals — zero-init stack slots, copy-semantics get/set/tee#856
Merged
Conversation
…et/set/tee
The `-b aarch64` backend loud-declined every function with a `local`
declaration beyond its params (`local.get {i}: non-param locals not yet
supported`), blocking almost all real functions.
Lower non-param locals to a stack-slot frame:
- Each non-param local (index >= num_params) gets a fixed 8-byte slot at
`[sp, #(idx-num_params)*8]`. The frame is sized to a 16-byte multiple
(SP alignment) and only materialized when the function actually has one
or more non-param locals — a params-only function is byte-identical to
before (localizing guard, verified by `no_non_param_locals_is_byte_identical`).
- Prologue lowers SP and ZERO-INITs every slot with `str xzr` (WASM locals
default to 0). 64-bit stores clear the whole slot so both i32 and i64 locals
read back intact.
- `local.get` LOADS the slot into a FRESH temp (`ldr`) — a copy, never the home
location — so a later `local.set` of the same index cannot alias a value
already on the value stack. This is the aliasing miscompile the register-by-
reference param path would hit; gated by `local_get_set_get_no_alias`
(0+5=5, not 5+5=10).
- `local.set` stores + pops; `local.tee` stores WITHOUT popping.
- Writing a PARAMETER is LOUD-DECLINED (params live in arg registers by
reference; homing written params is a later increment) — never a silent write.
- FP non-param locals fall out honestly: `pop_gp`/the tee file-check reject an
FP value (local types are not threaded to the backend), so an FP local is an
honest error, not wrong code.
Encoder: add `str_x_imm`/`ldr_x_imm` (64-bit unsigned-offset load/store) and
`sub_imm64`/`add_imm64` (SP adjust), each cross-checked against
`clang -target aarch64-linux-gnu` ground truth.
Does not touch the ARM/RV32 backends or any frozen anchor (separate backend).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
`aarch64_locals_851_differential.py` compiles `aarch64_locals_851.wat` with `synth compile -b aarch64` and diffs every exported function against wasmtime, under TWO oracles: unicorn (portable, CI path) and — on an arm64 host — native MAP_JIT (the real silicon calls the emitted `.text` through ctypes). Every function uses a non-param local, so: - BEFORE #851 the compile DECLINES all 8 (`local.get {i}: non-param locals not yet supported` / `unsupported wasm op: LocalSet/LocalTee`) — the harness detects the decline and reports RED. Verified against a pre-fix binary. - AFTER #851 all 14 cases match wasmtime under both oracles — GREEN. The load-bearing cases (pass a naive lowering, fail a correct one): - `get_set_get_no_alias` — local.get must COPY (returns 5, not the 10 a read-by-reference model gives). - `zero_init_read_first` — a local read before any write is 0. - `i64_local` — the 64-bit slot preserves full width. Does not touch ci.yml or the vendored `aarch64_matrix.sh` (lane L1 owns those). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…e case - selector.rs module doc: the "still declined" list no longer claims non-param locals are unsupported; documents the #851 slot model + the remaining written-param / FP-local declines. - FEATURE_MATRIX.md AArch64 row: adds the non-param-locals capability and the written-param / FP-local declines. - New unit test `local_across_void_block_balances_sp` + differential function `local_across_block`: a non-param local read/written across a void `block` with an early `br_if` out, asserting exactly one prologue `sub sp` and one epilogue `add sp` (SP balanced on both the taken and fall-through paths, the block End takes the ctrl-pop path with no epilogue). Green across all three oracles (wasmtime/unicorn/native): param0=0→2, param0!=0→1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The
-b aarch64backend loud-declined every function with alocaldeclaration beyond its params (local.get {i}: non-param locals not yet supported), blocking almost all real functions. This lane (L2 of the v0.51 hub) lowers non-param locals.Closes part of #851.
How
Lowering (
selector.rs):>= num_params) gets a fixed 8-byte stack slot at[sp, #(idx-num_params)*8]. The frame is sized to a 16-byte SP-aligned multiple and only materialized when the function actually has one or more non-param locals — a params-only function is byte-identical to before (localizing guard).str xzr(WASM locals default to 0). 64-bit stores clear the whole slot so both i32 and i64 read back intact.local.getLOADS the slot into a fresh temp (ldr) — a copy, never the home location — so a laterlocal.setof the same index cannot alias a value already on the value stack. This is the aliasing miscompile the register-by-reference param path would hit.local.setstores + pops;local.teestores without popping.ret. Interacts correctly with the Feature challenge: add an aarch64 (host-native) backend so output runs and debugs natively on arm64 dev machines #538-cf void blocks: the blockEndtakes the ctrl-pop path (no epilogue), so SP is balanced on every path.Encoder (
encoder.rs): addsstr_x_imm/ldr_x_imm(64-bit unsigned-offset load/store) andsub_imm64/add_imm64(SP adjust), each cross-checked againstclang -target aarch64-linux-gnuground truth.Gate evidence (RED-first)
scripts/repro/aarch64_locals_851_differential.pycompilesaarch64_locals_851.watwithsynth compile -b aarch64and diffs every exported function against wasmtime under two oracles: unicorn (portable/CI) and — on an arm64 host — native MAP_JIT (the real silicon calls the emitted.textthrough ctypes).non-param locals not yet supported/unsupported wasm op: LocalSet/LocalTee); the harness detects the decline and reports RED. Verified.Load-bearing cases (pass a naive lowering, fail a correct one):
get_set_get_no_alias—local.getmust COPY (returns 5, not the 10 a read-by-reference model gives).zero_init_read_first— a local read before any write is 0.i64_local— the 64-bit slot preserves full width.local_across_block— non-param local read/written across a void block +br_if; SP balanced on both paths.Verification
cargo test -p synth-backend-aarch64— 65 passed (incl. 7 new locals tests).cargo clippy -p synth-backend-aarch64 --all-targets -- -D warnings— clean.cargo fmt— clean.frozen_codegen_bytes10/10 (separate backend, ARM codegen has no dep on this crate).aarch64_m2_538_differential.py182/182 still green.Coordination
Does NOT touch
.github/workflows/ci.ymlorscripts/repro/aarch64_matrix.sh(lane L1 owns those — memory + gate vendoring + CI wiring). This lane touches onlysynth-backend-aarch64source + its own new differential (aarch64_locals_851.{wat,py}) + doc prose.🤖 Generated with Claude Code
https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L