fix(#851): home written parameters on aarch64 — and fix the conditional-param-write silent miscompile - #971
Merged
Merged
Conversation
Adds `aarch64_param_homing_851.wat` + its wasmtime/unicorn/native-MAP_JIT differential. Every exported function WRITES one of its own parameters, which the aarch64 selector loud-declines in a LEAF function today, so `--all-exports` exits non-zero — the RED state, captured before any implementation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
… hit the API session limit Committed by the hub coordinator so the work survives the interruption. NOT reviewed and NOT complete: the lane stopped mid-task, so this is a restore point, not a claim that anything is verified. The lane resumes from here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ixture `cond_write_param` / `cond_write_param_loop` cover the #457 param-count inference hole found while writing this differential: a param written before it is read in LINEAR op order — but only CONDITIONALLY — was reclassified as a zero-init non-param local and compiled SILENTLY WRONG. Measured on c2f9d72: `cond_write_param(0, 42)` returns 0, wasmtime returns 42. `param_write_across_call` now evaluates its `call` with an EMPTY value stack: a live temp across a call is a separate, still-live aarch64 decline, so the old shape would have read as a regression of this lane rather than a guard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
… lower Two changes, both in the param-homing mechanism: 1. `selector.rs` — widen the homing predicate from `is_non_leaf && references_param` to `(is_non_leaf && references_param) || writes_param`. A written param now gets an 8-byte home slot filled from its argument register in the prologue, and `local.get` loads a fresh copy, so the aliasing hazard the decline was guarding against (a param write clobbering a value already on the value stack) is structurally impossible. `writes_param` is a SUBSET of `references_param`, so exactly one class changes behaviour: leaf functions that write a param, every one of which loud-declined before. Non-leaf homing is byte-identical. The now-unreachable decline arms are kept as loud INTERNAL invariant errors rather than deleted. 2. `backend.rs` — fix a silent miscompile in the param-count inference. With a declared count in hand the bound is now `min(highest REFERENCED index + 1, declared)`, not the read-first heuristic. The old rule demoted a param that was written before it was read in LINEAR op order to a ZERO-INITIALIZED non-param local, so a CONDITIONAL write emitted wrong code instead of declining. Measured on c2f9d72: cond_write_param(0, 42): wasmtime=42 synth=0 `min` (rather than plain `declared`) preserves the existing leniency for a function with >8 declared params that only touches the first few. Differential: 44/44 cases match wasmtime under unicorn AND the native arm64 MAP_JIT oracle, floor `emulations >= 44` measured exactly 44. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
RQ-57-A64PARAM landed param homing; this is the ledger/gate half that the
implementation forces, plus the doc-honesty corrections the change makes due.
Ledger closes (each was RED before this commit, by the gate's own stale-entry
check — captured before editing):
* cross_backend_op_parity.rs — `local.set+get(param)` and `local.tee(param)`
reported STALE with `arm_ok=true, aarch64_ok=true`. Deleted; the "FOUR
below" prose is now TWO (both halves of bulk memory).
* aarch64_m2_decline_538.py — "local.set on a param" printed
`BUG ...: compiled — expected a LOUD decline`. Deleted.
Decline honesty (moved, not deleted). The FLOAT-param decline is now reachable
from a SECOND direction: a LEAF function homes as soon as it WRITES a param, so
a declared float param declines there too. Added as its own probe entry AND a
unit test, both asserting the "FLOAT parameter" machine reason. The old label
("homing needs an FP store") was FALSE — the encoder has `str s/d` since v0.54
L2; the real reason is that the home-slot model is single-register-file.
Three floors were vacuous — each tolerated a large silent regression:
* aarch64 parity leg 60 -> 108 (measured; old floor was ~half the real count)
* decline matrix 12 -> 14 (measured)
* matrix accepted ops 32 -> 61 (measured; gale's stale 0.50.x baseline)
Also wires the param-homing differential into CI (it was marked
`ci-status: wired` but had no step) and corrects a stale comment block in
selector.rs that still said "params stay read-only, see the LocalSet/LocalTee
decline below" — that decline no longer exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…t parsing An UNQUOTED GitHub Actions step name containing " #" is truncated by YAML at the comment marker. 14 other steps in this file already display as a bare "Run" in the Actions UI for exactly this reason, which makes a red aarch64-oracle job hard to attribute to a specific oracle. Quote the step this lane adds so at least it stays legible; sweeping the other 14 is cosmetic and deliberately left out of this lane. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…ming-851 # Conflicts: # crates/synth-backend-aarch64/src/selector.rs
avrabe
added a commit
that referenced
this pull request
Aug 14, 2026
…MS (#970) (#972) Six artifacts still read `proposed` while their work is on main. Re-graded to `implemented`, so the release-readiness query means something: RQ-57-SKIPEXIT #952 (#960) declined REQUESTED export exits non-zero RQ-57-PROVGAP #944 (#967) verified origins for introduced branches RQ-57-DOCSWEEP #946 (#968) Tiers 2+3 honesty sweep RQ-57-SPWILD #946 (#969) writes_sp 175-of-222 wildcard expanded RQ-57-A64PARAM #851 (#971) aarch64 written-param homing RQ-561-ZEROMEM #953 (v0.56.1) shipped two releases ago, never re-graded NEW: RQ-57-COUNTPARAMS (#970). It has no artifact because nobody planned it — the #851 lane found it while fixing the aarch64 instance of the same shape. An unplanned finding with no artifact is invisible to the release query, which is exactly how #933 slipped a release, so it gets one now. What it records that the issue alone does not: * The severity differs by BACKEND and both halves are stated at the confidence they were established. RISC-V is EXECUTED-confirmed: with a poisoned stack the function returns 0xDEADBEEF — an UNINITIALISED stack slot, i.e. previous frame contents. That is information disclosure, not a wrong value. ARM's exact wrong value was INFERRED FROM DISASSEMBLY, never executed; the artifact says so, because the #851 lane's first draft stated it as measured and the advisor caught it. * ARM is only INCIDENTALLY correct on the simple shape (a merge-point `str r1,[sp]` catches the still-live param) and breaks once a call clobbers the param register — so "ARM looks fine" is not evidence. * Red-first must be per-backend BY EXECUTION. Assuming one backend's evidence transfers is what left this latent after the aarch64 fix. * The named residual survives: the `None` branch of `current_func_param_count` still uses the unsound heuristic — unreachable from the CLI, reachable via the direct `compile_function` API. Per the user's direction this ships in v0.57 rather than as a v0.56.3 patch. rivet: errors unchanged (50 before and after); warnings 164 -> 166, which is the two every artifact in this file carries (trailer-reference naming, and the `verifies` link that lands with the test) — verified identical for RQ-57-SPWILD and RQ-57-GPIO, so this is one more artifact of the same shape, not a new defect. claim_check 43/43. Refs #970 Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
avrabe
added a commit
that referenced
this pull request
Aug 14, 2026
…of them mine
Cold review of the assembled release. Nothing blocked the tag; everything below
is accuracy. Four of the eight were errors in the CHANGELOG I had just written,
which is the reason the review exists.
THE GENERALIZABLE FINDING, and it is pointed given this release's theme:
`check_generated_fresh` byte-compares the RENDERED FEATURE_MATRIX against the
TEMPLATE. `render_feature_matrix` only substitutes `{{...}}` fields, so the gate
proves the render is faithful to the template — and NEVER that the template is
faithful to the code. Every stale number below lives in template prose no
substitution touches. In a release titled "the checkers were the defects", that
is the checker that cannot fail. Three independent stale numbers survived a
green 43/43.
USER-FACING FALSE, verified by compiling rather than by reading:
FEATURE_MATRIX listed "writing a PARAM local in a LEAF function" as a LOUD
DECLINE on aarch64. #971 shipped exactly that. A leaf `local.set` on a param
compiles: 32 bytes of machine code, exit 0. Also corrected in the same row:
homing is no longer non-leaf-only, and the float-param decline widened with
it. Fixed in the TEMPLATE (the render is generated) + regen.
MY CHANGELOG ERRORS:
* "145 of the 175 pre-declined / 30 reachable" matched no partition. The
shipped source (wcet_loops.rs:1232) says 142 give up with `true`, leaving
33. Re-derived: 142/33. Corrected.
* "demoted to a zero-initialised local" is wrong for the two backends the
entry is about — zero-init is gated on first-access-being-a-READ, and in
the cond-write shape the first access IS the write, so nothing initialises
the slot. That is WHY it reads poison; the old wording made an
information-disclosure bug sound like a benign wrong value, and contradicted
the entry's own next sentence.
* "Nine artifacts" — there are ten, and RQ-57-DOCSWEEP (#946/#968) had NO
CHANGELOG entry at all despite touching CLAUDE.md, coq/STATUS.md,
PROJECT_STATUS.md, the matrix template and eight source files. Added.
* "Five in-tree oracles took that opt-in" — eight scripts plus three Rust
tests. All eight carry floors, but `i64_param_518_riscv_loudskip`'s is
`compiles >= 1`, which is a floor and NOT the "tight" one the paragraph
claimed for the set. Named rather than folded into the claim.
STALE COUNTS (the template-prose class above):
ORACLE_WIRING.md, the matrix template and claims.yaml all said "137 oracles /
295,621 emulator entries". Re-derived independently — and the reviewer's
number and mine agree exactly: 144 oracles / 296,059. Both `count-min` pins
moved 137 -> 144 with them (same `emulations >=` pattern, two sibling claims);
the pinned verbatim texts moved too, or the ledger would have gone red
against its own corrected doc.
REVERSE STALENESS (a doc calling SHIPPED work missing):
`synth verify` declines shift rules citing "SMT modeling of the variable-shift
register encoding is an open gap". #975 CLOSED that gap — it modelled
LslReg/LsrReg/AsrReg/RorReg as Rm<7:0> (ARMv7-M A7.7.68/70/12/117) and moved
five lowerings Invalid -> Verified. Both comments corrected to say what is
true: the modelling gap is closed, the remaining decline is a WIRING residual.
Behaviour deliberately unchanged — rewiring the rule table is a
verification-surface change, not release assembly. Filed as #981.
ARTIFACT:
RQ-57-PROVGAP still asserted "9 object branches with no WASM origin" as fact
while its own PR disproved it. Outcome recorded, as RQ-57-BACKFILL already did.
docs/architecture/CRATE_STRUCTURE.md said 18 crates; there are 19. A
RECURRENCE — PROJECT_STATUS.md cites this exact drift as why it was gutted in
the #946 sweep, and one file over it was live again.
Gates after: claim_check 43/43, check_version_pins OK at 0.57.0,
cargo check -p synth-cli rc=0.
Refs #980, #981
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
avrabe
added a commit
that referenced
this pull request
Aug 14, 2026
* chore(release): v0.57.0 assembly — "the checkers were the defects" Nine artifacts. In five of them the bug was in the machinery that checks the compiled code, not in the compiled code: #975 ArmSemantics silently no-oped 87 of 222 ops — a Rocq-proved, default-on rotl rule was "validated" by a model that executed neither of its instructions #976 the gpio differential CANNOT discriminate the miscompile it guards — complementary conditions, so no input to that driver can #969 writes_sp claimed exhaustiveness over a wildcard absorbing 175 of 222 #967 the "9 unattributed branches" were manufactured by witness's own hardcoded divergence text #979 the prescribed release: back-fill would have written 32 false entries, with the one correct pre-existing value beside them as the disproof The unifying property is that each of those checks COULD NOT FAIL. This release makes them able to fail and proves it by making them fail on purpose. Also fixed, and the most severe item: #974 — a conditionally-written parameter was demoted to a zero-init local on ARM and RISC-V. Exit 0, no decline, wrong code; on RISC-V it reads an UNINITIALISED stack slot (0xDEADBEEF under a poisoned stack), an information-disclosure shape. ARM behaves identically — which the issue predicted otherwise, and only execution settled. Release surfaces, all four swept and checker-confirmed at 0.57.0: Cargo.toml [workspace.package] + 10 path-dep pins MODULE.bazel, npm/package.json, Cargo.lock (cargo metadata) scripts/check_version_pins.py: OK Derived artifacts regenerated (--emit-status): artifacts/status.json, docs/status/FEATURE_MATRIX.md. Claim gate: 43/43. Open by design, named not hidden: #973 (ARM select miscompile, found only because a lane compiled ARM fixtures — which CI never does), #977 (ELF-magic flake, second sighting), #938 (breaking object 0.x-minor bump, auto-merge disabled), #912 (open with four remaining: items). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * fix(release): act on the v0.57.0 cold review — 8 accuracy defects, 4 of them mine Cold review of the assembled release. Nothing blocked the tag; everything below is accuracy. Four of the eight were errors in the CHANGELOG I had just written, which is the reason the review exists. THE GENERALIZABLE FINDING, and it is pointed given this release's theme: `check_generated_fresh` byte-compares the RENDERED FEATURE_MATRIX against the TEMPLATE. `render_feature_matrix` only substitutes `{{...}}` fields, so the gate proves the render is faithful to the template — and NEVER that the template is faithful to the code. Every stale number below lives in template prose no substitution touches. In a release titled "the checkers were the defects", that is the checker that cannot fail. Three independent stale numbers survived a green 43/43. USER-FACING FALSE, verified by compiling rather than by reading: FEATURE_MATRIX listed "writing a PARAM local in a LEAF function" as a LOUD DECLINE on aarch64. #971 shipped exactly that. A leaf `local.set` on a param compiles: 32 bytes of machine code, exit 0. Also corrected in the same row: homing is no longer non-leaf-only, and the float-param decline widened with it. Fixed in the TEMPLATE (the render is generated) + regen. MY CHANGELOG ERRORS: * "145 of the 175 pre-declined / 30 reachable" matched no partition. The shipped source (wcet_loops.rs:1232) says 142 give up with `true`, leaving 33. Re-derived: 142/33. Corrected. * "demoted to a zero-initialised local" is wrong for the two backends the entry is about — zero-init is gated on first-access-being-a-READ, and in the cond-write shape the first access IS the write, so nothing initialises the slot. That is WHY it reads poison; the old wording made an information-disclosure bug sound like a benign wrong value, and contradicted the entry's own next sentence. * "Nine artifacts" — there are ten, and RQ-57-DOCSWEEP (#946/#968) had NO CHANGELOG entry at all despite touching CLAUDE.md, coq/STATUS.md, PROJECT_STATUS.md, the matrix template and eight source files. Added. * "Five in-tree oracles took that opt-in" — eight scripts plus three Rust tests. All eight carry floors, but `i64_param_518_riscv_loudskip`'s is `compiles >= 1`, which is a floor and NOT the "tight" one the paragraph claimed for the set. Named rather than folded into the claim. STALE COUNTS (the template-prose class above): ORACLE_WIRING.md, the matrix template and claims.yaml all said "137 oracles / 295,621 emulator entries". Re-derived independently — and the reviewer's number and mine agree exactly: 144 oracles / 296,059. Both `count-min` pins moved 137 -> 144 with them (same `emulations >=` pattern, two sibling claims); the pinned verbatim texts moved too, or the ledger would have gone red against its own corrected doc. REVERSE STALENESS (a doc calling SHIPPED work missing): `synth verify` declines shift rules citing "SMT modeling of the variable-shift register encoding is an open gap". #975 CLOSED that gap — it modelled LslReg/LsrReg/AsrReg/RorReg as Rm<7:0> (ARMv7-M A7.7.68/70/12/117) and moved five lowerings Invalid -> Verified. Both comments corrected to say what is true: the modelling gap is closed, the remaining decline is a WIRING residual. Behaviour deliberately unchanged — rewiring the rule table is a verification-surface change, not release assembly. Filed as #981. ARTIFACT: RQ-57-PROVGAP still asserted "9 object branches with no WASM origin" as fact while its own PR disproved it. Outcome recorded, as RQ-57-BACKFILL already did. docs/architecture/CRATE_STRUCTURE.md said 18 crates; there are 19. A RECURRENCE — PROJECT_STATUS.md cites this exact drift as why it was gutted in the #946 sweep, and one file over it was live again. Gates after: claim_check 43/43, check_version_pins OK at 0.57.0, cargo check -p synth-cli rc=0. Refs #980, #981 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * style: rustfmt the #975 decline-reason comment (indent 14 -> 12) My own miss: I ran cargo check on the edited file but not cargo fmt, and Format is a required context. The comment content is unchanged — only the indentation rustfmt wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
Closes the aarch64
local.set/local.tee-on-a-parameter decline by HOMING written params — and fixes a silent miscompile found while writing the fixture.Two changes
1. Param homing (the requested capability).
crates/synth-backend-aarch64/src/selector.rswidens the homing predicate fromis_non_leaf && references_paramto(is_non_leaf && references_param) || writes_param. A written param gets an 8-byte home slot filled from its argument register in the prologue;local.getloads a fresh copy, so a param write cannot alias a stacked value — the exact hazard the old decline guarded.writes_param ⊆ references_param, so exactly one class changes: leaf functions that write a param, all of which declined before. Non-leaf homing is byte-identical. The now-unreachable decline arms are kept as loud internal-invariant errors rather than deleted.2. A silent miscompile in
backend.rs(not in the original scope).num_paramswasmin(count_params(ops), declared), andcount_paramscounts only indices read before written in linear op order. A param written before it is read — but only conditionally — was demoted to a zero-init non-param local and compiled wrong instead of declining. Nowmin(highest REFERENCED index + 1, declared); theminpreserves the >8-declared-params leniency that plaindeclaredwould have broken (verified: a hardnum_params > 8guard sits above the indexing, so plaindeclaredwould turn a lenient compile into a decline).Evidence
Negative control — the most important artifact here. Reverting only the
referenced_localsline and rebuilding:All three failures are
cond_write_param, the exact shape the fix guards; the other 41 stay green, so the control is specific rather than a blanket break. They are miscompiles, not declines — which is the point: a decline-matrix probe could never have caught this, only an execution oracle. Both oracles (unicorn and the native arm64 MAP_JIT path) agree on the wrong value. Restored, and back to 44/44.Pre-edit RED on both gates (captured before touching them — each gate self-reds, no new assertion needed):
cross_backend_op_parity.rsflagged both entries stale by its own check:local.set+get(param) — ledgered as an ARM/aarch64 divergence but both now agree (arm_ok=true, aarch64_ok=true); delete the entry(same forlocal.tee(param)).aarch64_m2_decline_538.pyprintedBUG local.set on a param: compiled — expected a LOUD decline→RESULT: FAIL (1 silent).Both are green after the ledger edits.
Ledger, decline honesty, and floors
Deleted the two stale parity entries; "leaving the FOUR below" is now TWO (both halves of bulk memory). Removed the closed
local.set on a paramdecline entry.The FLOAT-param decline MOVED rather than disappeared. Homing on
writes_parammakes a leaf function that writes a param hit the float check too, so a declared float param now declines from a second direction. Added as its own decline-probe entry and a unit test, both asserting theFLOAT parametermachine reason. The old label ("homing needs an FP store") was false — the encoder hasstr s/dsince v0.54 L2; the real reason is that the home-slot model is single-register-file. Corrected in the probe, the selector message, and the module doc.Three floors were vacuous — each tolerated a large silent regression. Raised to measured values:
at_parity)The old parity floor was ~half the real count, so the entire param-write class could have regressed to a decline unnoticed. Caveat: the matrix floor was measured locally by running the script (61 accepted, 355 checks, PASS, empty declined frontier), but the CI-side
sedextraction around it is unchanged and not exercised locally.Also wires
aarch64_param_homing_851_differential.pyinto CI — it was markedci-status: wiredbut had no step — and corrects a stale comment block inselector.rsthat still read "params stay read-only, see the LocalSet/LocalTee decline below".Decline list re-verified, not assumed
I removed
call_indirectand "reading own params across a call" from the selector module doc's decline list. Both verified by compiling the shapes (exit 0each), not by inspection.Residual
The
Nonebranch ofcurrent_func_param_countstill uses the unsoundcount_paramsheuristic. Unreachable from the CLI —func_arg_countsis populated for every function index (imports first, then local), so.get(func.index)is alwaysSome— but the directcompile_functionAPI can still reach it. Documented at the function; not fixed here.Related finding — filed separately as #970
The same
count_params(ops).min(declared)shape exists in ARM (arm_backend.rs:393) and RISC-V (effective_num_params). RISC-V is confirmed broken by execution: on the fall-through path it reads an uninitialised stack slot (returns the0xDEADBEEFpoison under unicorn), which is an information-disclosure shape on top of the miscompile — worse than the aarch64 instance, which returned a deterministic0. ARM is incidentally correct on the simple shape and miscompiles once the param register is clobbered by a call. Not fixed here — each backend has a different local/slot model and needs its own differential.Gates
cargo test --workspace— exit 0, 2909 tests, 0 failures (re-run without a pipe; the earlier ambiguous result wastail's exit code, not cargo's)cargo clippy --workspace --all-targets -- -D warnings— exit 0, no warningscargo fmt --check— exit 0python3 scripts/claim_check.py claims.yaml— 43/43Base is current with
origin/main(mergedeea92e11, #967).🤖 Generated with Claude Code
https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L