Skip to content

feat(reach): RQ-65-MVPCORE (#1017) — MVP core re-derived on the executed path: single-module .wast routing (oracle first), merge refusals, start-function invocation; 14/114 -> 12/80 - #1232

Merged
avrabe merged 5 commits into
mainfrom
feat/mvpcore-1017
Sep 9, 2026

Conversation

@avrabe

@avrabe avrabe commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

RQ-65-MVPCORE (#1017) — the MVP-core row, re-derived on the path the oracles execute

Refs #1017, #1225, #1046, #1217. Filed on the way: #1225, #1229, #1230, #1231.

The measured delta, stated plainly (per backend, ok / partial)

set backend before (main 580d53f9) after (this PR) delta ok
corrected MVP core, 80 files arm 13 / 31 12 / 21 -1
riscv 10 / 31 9 / 23 -1
aarch64 18 / 12 18 / 20 0
v0.63's MVP core, 114 files arm 14 / 39 12 / 22 -2
riscv 11 / 38 9 / 23 -2
aarch64 21 / 17 18 / 20 -3

The reach increment (start-function invocation) moved ZERO files on every backend. Its four sole-blocker files are multi-module (start, annotations, binary) or a multi-memory twin (start0). The gain is module-level, in the parity oracle's own line: start-function modules: 4 seen, 2 both-accepted and booted ..., 10 assertion(s) ok — 0 both-accepted on the compiler before it. That is the file-vs-module gap v0.63's caution predicted, measured rather than assumed.

The row went down, and every mover was inspected: losses are refusals of objects that were wrong (int_exprs i64 signatures, const 402 f32/f64-result modules, stack accessed globals, binary-leb128/memory/address/float_memory data — aarch64 refuses data-carrying modules on the real path since v0.53); gains are first real compiles (nop/loadok on arm once a real call_indirect table reached the backend; on aarch64 f32, f32_bitwise, f32_cmp, f64_bitwise, f64_cmp, float_misc, conversionsok, and 12 core files from whole-file refusal to partial).

Why both numbers in "14 / 114" were properties of the instrument

  1. Denominator. 31 multi-memory-proposal twins (address0, load0..2, memory_size0..3, data0/1, imports0..4, start0, ... — content-verified at 2..21 memories per module, suite commit 4b24564) and 3 relaxed-SIMD lane files sat in MVP core because spec_compile_census.py's family regexes predate the Wasm 3.0 merge (^multi.?memory|^memory_multi matched none; ^data\b cannot match data0). MVP core is 80 files; multi-memory is a family row and a FAMILY_OK_PINS entry. v0.63's top arm blocker (17 files sole) was a multi-memory decline mis-filed as scalar-core.
  2. Numerator (The spec-suite census compiles .wast files on a path no oracle executes — it never sets R9, drops data segments and global initializers at exit 0 #1225, the "correct by accident" finding). synth compile file.wast — the census invocation — took the multi-module MERGE branch, written for synth's i32-only fixture suite and saying so beside every Vec::new() it passes. It handed the backends empty data segments, empty globals (the Cortex-M startup never wrote R9), i32-only signature tables and a default aarch64 substrate. One module compiled as .wast vs .wat: no data bytes and no R9 write on the .wast leg while get_g was ldr.w r4, [r9] on both; tests/wast/i64_arithmetic.wast's i64.add on two i64 params lowered as adds r3, r0, r1 (param 0's low half plus its own high half). Exit 0, counted ok. The parity oracle compiles per-module .wat — the other path — so the number the project planned from was measured on objects no oracle runs (the shape The spec-suite gap was declared closed by a COMPILE census — the behavioural half stayed open and nothing sampled it until #197 #1217 asks to sweep for).

Oracles landed FIRST, both red before

  • scripts/repro/wast_single_module_path_identity_1225.py (new, wired into the Spec Suite workflow, # ci-checks: compiles >= 600): a single-module .wast must compile to exactly the bytes its module compiles to alone, three backends. Red on the pre-fix driver: arm 49 / riscv 33 / aarch64 56 of 140 mismatched. Then the driver routes a single-module .wast through the single-module path → 140 / 140 / 140 identical, RESULT PASS; the multi-module merge REFUSES a module carrying active data segments, accessed globals or a non-i32 signature over the functions it would retain (refuse_wast_merge_unrepresentable).
  • selector_parity_197_differential.py stopped declining (start ...) modules and gained START_MODULES_FLOOR. Red on the pre-invocation compiler (--only start: 4 seen, 0 both-accepted, both legs module-decline). Then the self-contained Cortex-M Reset_Handler BLs the start function after the data copy and the R9 table, before the entry BLX r0 (a BL, never BLX r0 — the oracle's boot scanner stops at the first blx r0), patched post-layout via encode_thumb_bl; the closure is seeded with it. Green after: 2 both-accepted and booted, 10/10 assertions ok (get observes three incs from the start function on both selectors and in wasmtime), 0 divergences. Every other path — --relocatable, ET_REL degradation via imports (checked after compilation), an imported start, the single-function path, A32, RISC-V, AArch64 (neither of the last two emits a startup) — keeps the ALL backends SILENTLY DROP the (start ...) function — exit 0, never compiled, never run (same silent-drop class as #1041) #1046 refusal; start_section_refusal_1046.rs pins both halves (10/10, two new refusals).

Declined with measured reasons (docs/status/SPEC_FAMILY_CENSUS.md, ranked on the v0.65 path)

Pins moved with the measurement, together

Census PINS / AT_LEAST_ONE_EXPORT / FAMILY_OK_PINS (whole-suite ok 22 / 12 / 27 → 16 / 9 / 21; at-least-one-export 84 / 74 / 57 → 47 / 43 / 47; other_error 0 / 0 / 0 — the VCR-MEM-002 multi-memory (#406) decline family reaches the census for the first time and is classified by its tag), README row, feature_matrix.md.tmpl (FEATURE_MATRIX.md regenerated by claim_check.py --emit-status), claims.yaml (SYNTH-SPEC-FAMILY-MVP-CORE, SYNTH-SPEC-SUITE-CENSUS-README/-MATRIX, a new count-min on the identity oracle's wiring). Emulation floor re-derived on the rebased tree by oracle_wiring_check.py: equal to the ci.yml/claims pin, so it does not move (the new oracle declares a compiles floor).

Note on generation: SPEC_FAMILY_CENSUS.md is TRANSCRIBED from the census's per-family stdout (the v0.63 cold review corrected "generated" to "transcribed"; FAMILY_OK_PINS in the script is the gate that reddens the census when a family count moves). FEATURE_MATRIX.md IS generated and was regenerated from the template.

Gates run on the rebased tree (761c6922 + this branch), release binary rebuilt there

  • scripts/spec_compile_census.py: RESULT PASS on the new pins, all three backends (MVP core 80 files: arm 12 / 21, riscv 9 / 23, aarch64 18 / 20; other_error 0 / 0 / 0).
  • scripts/repro/wast_single_module_path_identity_1225.py: RESULT PASS, 140 / 140 / 140 identical, 420 file-backend pairs compared.
  • scripts/repro/selector_parity_197_differential.py, full corpus: RESULT PASS — 330 modules both-accepted, 19155/19155 assertions in parity, 622 pinned divergences across 103 pins (RQ-65-DECLINE's converted pins merged), 0 unpinned; start-function modules: 4 seen, 2 both-accepted and booted ..., 10 assertion(s) ok, 0 divergences. (Pre-rebase tree: also PASS, 722 pinned across 108 pins.)
  • cargo test --workspace: 80 test binaries, 1293 passed, 0 failed.
  • cargo clippy --workspace --all-targets -- -D warnings: clean. cargo fmt --check: clean.
  • scripts/status_evidence_check.py: 0 failures (5 delivery-shaped commits in the window, 5 attributed; live floor restated 0 times). scripts/claim_check.py claims.yaml: 63/63 hold; --emit-status leaves every generated file unchanged.
  • scripts/oracle_wiring_check.py: every script declared and wired; the derived emulation floor equals the ci.yml/claims pin, so it does not move.
  • rivet validate: 40 errors — the pre-existing cross-repo baseline; this artifact adds no links.

Both pushes went through one && chain (fmt, clippy -D warnings, evidence check, claim check, push).

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

avrabe and others added 2 commits September 9, 2026 06:37
…ted path: single-module .wast routing (oracle first), merge refusals, start-function invocation; 14/114 -> 12/80

Both numbers in v0.63's "MVP core 14 / 114" were properties of the
instrument. The denominator: 31 multi-memory-proposal twins and 3
relaxed-SIMD lane files sat in MVP core because the census's family
regexes predate the Wasm 3.0 merge — MVP core is 80 files, and
`multi-memory` is a family row. The numerator (#1225): the census's
`synth compile file.wast` took the multi-module MERGE branch, written for
synth's i32-only fixture suite, which handed the backends empty data
segments, empty globals (the Cortex-M startup never wrote R9) and
i32-only signature tables (`i64.add` on two i64 params lowered as
`adds r3, r0, r1`). Exit 0, counted `ok`, on a path no execution oracle
compiles.

Oracle first: `scripts/repro/wast_single_module_path_identity_1225.py`
(a single-module .wast must compile to exactly the bytes its module
compiles to alone; three backends; wired into the Spec Suite workflow)
was RED on the pre-fix driver (arm 49 / riscv 33 / aarch64 56 of 140),
then a single-module .wast is routed through the single-module path
(140/140/140 identical) and the merge refuses a module with active data
segments, accessed globals or a non-i32 signature over its retained
functions.

Reach increment, oracle first: the self-contained Cortex-M Reset_Handler
now BLs the (start ...) function after the data copy and the R9 table,
before the entry BLX r0. The parity oracle stopped declining start
modules and gained a floor that was red before (4 seen, 0 both-accepted)
and is green after (2 both-accepted, 10/10 assertions ok, 0
divergences). Every other path keeps the #1046 refusal.

MEASURED DELTA, per backend, on the corrected 80-file set: arm 13 -> 12
ok, riscv 10 -> 9, aarch64 18 -> 18 (old 114-file set: 14 -> 12,
11 -> 9, 21 -> 18). Every loss is a refusal of an object that was wrong;
every gain a first real compile. The start increment moved ZERO files
(its files are multi-module or multi-memory) — the file-vs-module gap
v0.63's caution predicted, measured. Pins, README, FEATURE_MATRIX
template, claims.yaml and the family doc move together; other_error
stays 0 (the #406 multi-memory decline family is classified by its
tag). Filed: #1229, #1230, #1231.

Refs #1017, #1225, #1046, #1217

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…-corpus parity PASS recorded

Fills `landed:` with the PR number (it did not exist until `gh pr create`
returned) and records the parity evidence measured on the rebased tree:
full corpus RESULT PASS, 330 modules both-accepted, 19155/19155 assertions,
622 pinned divergences across 103 pins, 0 unpinned, start-function line
2 both-accepted / 10 ok / 0 divergences. The CI job on the PR is the gate;
these runs are its pre-flight.

Refs #1017

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

avrabe commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Coordinator verification of #1225 — reproduced independently, and it is a silent miscompile of the plainest possible shape

Not a re-read of the lane's report. Compiled synth's own tests/wast/i64_arithmetic.wast with the
main binary (761c6922) and with this branch's, and decoded .text directly rather than trusting a
disassembler rendering.

The function is (func (export "add") (param i64 i64) (result i64) local.get 0 local.get 1 i64.add).
Under AAPCS, param0 is r0:r1 and param1 is r2:r3.

main (the census's own compile path):

e92d4010  push {r4, lr}
e0903001  adds r3, r0, r1     <- adds param0's LO to param0's HI
e0a11002  adc  r1, r1, r2     <- param0.hi + param1.lo
e1a00003  mov  r0, r3
e1a01001  mov  r1, r1
e8bd8010  pop {r4, pc}

this branch:

e92d4010  push {r4, lr}
e0900002  adds r0, r0, r2     <- param0.lo + param1.lo
e0a11003  adc  r1, r1, r3     <- param0.hi + param1.hi
e1a00000  mov  r0, r0
e1a01001  mov  r1, r1
e8bd8010  pop {r4, pc}

Every parameter was being homed at r_i — the narrow, one-register-per-local model — so a two-i64
add summed the two halves of the same argument. exit 0, no decline, no warning, and the census
counted the file ok.

A second symptom worth recording: the object produced by that path has no .symtab at all
(sections are .text, .shstrtab, .ARM.attributes), while the CLI's own success message tells the
user to run objdump -t to see symbols. Any consumer reading symbols from that object gets nothing.

On the headline number. "MVP core is 14/114" was wrong in both halves and this artifact says so:
the denominator carried 31 multi-memory twins and 3 relaxed-SIMD files that are not MVP core, and the
numerator counted files compiled on this path. Re-deriving on the executed path moves arm 14/114 to
12/80. That the corrected number is lower is the right outcome to publish.

And the reach increment gained ZERO files, reported as such — which is exactly what the artifact
demanded ("report the MEASURED delta including if it is zero") and what the v0.63 precedent predicted.
The gain is real but module-level: 4 start modules seen, 2 both-accepted and booted, 0 before.

Refs #1225, #1017

@avrabe

avrabe commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Coordinator: merge-order hazard between #1227 and #1232 — do not merge these in either order without re-deriving

Measured, not reasoned. Both PRs are green on their own base; together they break main.

#1227 pins #1226 red-first on an observable:

grep -Eq '^WIDTH-1226: known-open \(\.wast homes < \.wat homes for the same module text on 4 legs\)$'

Its fixture scripts/repro/home_alias_width_1226.wast is a two-module .wast — deliberately, because
the mis-homing lives in the multi-module merge path.

#1232 makes that merge path REFUSE any module it would mis-home (any(&module.func_params_i64)
"signature tables are i32-only, so an i64 parameter pair is mis-homed"). Compiling #1227's fixture with
#1232's binary:

exit=1   Refused ... i32-only ... mis-homed

So once #1232 lands, the fixture no longer compiles, the audit cannot measure homes= on it, and
#1227's WIDTH-1226 grep fails. Green PR CI on a stale base, merges clean, reddens main.

A correction to my own first reading, recorded because it changes the conclusion. I initially tested a
single-module (param i64 i64) (result i64) local.get 1 and found #1232 emitting the correct pair
(mov r0,r2; mov r1,r3), and concluded #1232 had fixed the mis-homing. Then I ran the same input on
main's binary and got identical bytes — body inference recovers the width in that shape, so the test
discriminated nothing. The defect is specific to the merge path, which is exactly why the fixture is
two-module. #1232 converts a silent mis-homing into a loud refusal; it does not thread the widths.

Consequences to settle before either merge:

  1. Merge order: feat(reach): RQ-65-MVPCORE (#1017) — MVP core re-derived on the executed path: single-module .wast routing (oracle first), merge refusals, start-function invocation; 14/114 -> 12/80 #1232 first, then fix(selector): RQ-65-ALIASCLASS (#1189) — enumerate every uncopied home-register consumer on four legs; fix aarch64 rotl (#1221) and i64-param local.set (#1222) red-first; pin #1223/#1226 open #1227 rebases and re-derives its #1226 observable against a tree
    where the multi-module fixture is refused. A refusal is not a homes= count, so the pin needs a
    different expression — or the fixture needs to reach the defect by a route feat(reach): RQ-65-MVPCORE (#1017) — MVP core re-derived on the executed path: single-module .wast routing (oracle first), merge refusals, start-function invocation; 14/114 -> 12/80 #1232 still compiles.
  2. Is .wast driver path threads NO declared param/result width tables — every i64/f32/f64 param or result in ANY .wast module (single-module too) is homed as i32 unless body inference recovers it (was: multi-module merge / representative table) #1226 still open? After feat(reach): RQ-65-MVPCORE (#1017) — MVP core re-derived on the executed path: single-module .wast routing (oracle first), merge refusals, start-function invocation; 14/114 -> 12/80 #1232 the multi-module path emits no wrong bytes because it emits
    nothing. That is decline-honesty, which this project counts as the correct trade — but it is a reach
    loss, not a width fix. My reading is that .wast driver path threads NO declared param/result width tables — every i64/f32/f64 param or result in ANY .wast module (single-module too) is homed as i32 unless body inference recovers it (was: multi-module merge / representative table) #1226 stays open and changes character from "silent wrong
    answer" to "refused", and fix(selector): RQ-65-ALIASCLASS (#1189) — enumerate every uncopied home-register consumer on four legs; fix aarch64 rotl (#1221) and i64-param local.set (#1222) red-first; pin #1223/#1226 open #1227's note that the arm direct selector: local.set/local.tee of a register-homed i64 PARAM writes only the lo half (leaf functions), and the #989 snapshot can land on val's hi #1222 pair-write never fires on .wast still stands.
    The lane that owns it should confirm on the merged tree rather than take my word.

Refs #1226, #1225, #1222

…n carry its own evidence

`start_on_arm_selfcontained_is_invoked_from_reset_handler` failed once in CI on
#1232 with a bare `expect("disasm names Reset_Handler")`, which says nothing
about why.

NOT reproducible locally. Ruled out, each by execution rather than reasoning:
  - flake / temp-dir race: 5 clean runs, temp dir removed each time, 10/10
  - filename collision between the file's 10 tests: every test has a distinct
    fixture and a distinct derived output name
  - feature unification under `cargo test --workspace` (the CI command, and the
    recorded trap that feature-gated builds behave differently): 10/10 green

So the cause is environment-specific and I will not guess at a fix. Make the
next failure produce the evidence instead:

  - assert the object EXISTS and is non-empty before disassembling it, naming
    the path and listing the directory — the compile asserting success does not
    prove the conventional path was written
  - replace the bare expect() with the disasm exit code, stdout/stderr sizes,
    and the first 1200 chars of what was actually produced

Also records the fact that made this confusing: `<Reset_Handler>:` is
SYNTHESISED by `synth disasm` from the startup layout — the self-contained
image carries no `.symtab` at all (verified: symtab ABSENT, disasm still prints
the label). Its absence therefore means the image is not the self-contained
shape, not that a symbol is missing.

Refs #1017

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

avrabe commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Correction: my earlier "correction" was wrong — #1232 DOES fix the single-module .wast mis-homing

I posted a hazard note saying I had first concluded #1232 fixes the mis-homing, then "corrected"
myself after finding main emitting identical bytes, and concluded the defect was specific to the
multi-module merge path. That correction was the error. The original reading was right.

Cause: both worktrees shared one CARGO_TARGET_DIR, so ./target/debug/synth belonged to whichever
tree built last. When I believed I was running main's binary I was running #1232's.

Re-measured with each branch built into its own target dir, same input
((module (func (export "f") (param i64 i64) (result i64) local.get 1)), a single-module .wast):

binary .text verdict
main e92d4010 e1a00001 e8bd8010 mov r0, r1mis-homed
#1232 e92d4010 e1a00002 e1a01003 e8bd8010 mov r0, r2; mov r1, r3correct pair

So the ALIASCLASS lane's mechanism note was accurate as written: ANY .wast mis-homes on main,
single-module included, because the .wast decode arm threads no declared-width tables. #1232 fixes
the single-module case outright and refuses the multi-module case.

What this does and does not change:

Method note, since it cost two wrong claims: never compare two branches' binaries through a shared
CARGO_TARGET_DIR.
Build each into its own, or you are comparing a tree against itself.

Refs #1226, #1225, #1017

… on the host's objdump; split it

Root cause, from the diagnostics added in the previous commit rather than from
a guess:

  disasm exit: Some(1), stdout 211 B, stderr 77 B
  INFO Detected triple: thumbv7m-none-eabi
  objdump: can't disassemble for architecture UNKNOWN!
  Error: objdump failed

`synth disasm` SHELLS OUT to `objdump` (main.rs:8311, whose own comment says
"works on macOS with Apple LLVM"). The GitHub-hosted runners carry a plain
x86-64 binutils with no ARM support, so the disassembly the test parsed never
existed there. It passed on macOS because Apple's objdump is LLVM's. This is
the repo's recorded trap — disasm TEXT is host-dependent — reached through a
CLI subcommand rather than directly.

Split the test along that seam:

  HOST-INDEPENDENT, runs everywhere: the compiler's own acceptance decision,
  "the self-contained image's Reset_Handler invokes it before the entry call".
  This is what pins the #1046 refusal from coming back.

  BYTE-SHAPE, only where the disassembler works: the BL-before-BLX check, with
  an explicit NOTE printed when it is skipped, naming the disasm exit code. A
  silent skip would be the vacuity this release exists to find.

The execution proof was never this test and the doc comment already said so:
`selector_parity_197_differential.py` boots this very startup under unicorn and
is CI-wired.

Two things measured rather than assumed while fixing it:
  - the acceptance line goes to STDOUT (2917 B stdout / 0 B stderr on success);
    my first attempt read `stderr(&out)` and failed locally, which is why it was
    run locally before pushing
  - the self-contained image carries NO `.symtab`; `<Reset_Handler>:` is
    synthesised by the disassembler, so its absence never meant a missing symbol

10/10 locally.

Refs #1017

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

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.13450% with 22 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-cli/src/main.rs 87.13% 22 Missing ⚠️

📢 Thoughts on this report? Let us know!

… 1.98 rejects

`clippy::useless_borrows_in_formatting` on the `eprintln!` argument added by the
previous commit. My local gate missed it because I ran `-p synth-cli` while CI
runs `--workspace`, and — the part that would have bitten either way — local
clippy is 0.1.96 against CI's 1.98, so the lint does not exist here. That drift
is a recorded trap in this repo; the mitigation is to scan the whole diff for
the reported pattern rather than fix the one reported line, which I did: the
only other `&`-prefixed additions are a function argument and a slice literal,
neither a format argument.

Refs #1017

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
@avrabe
avrabe merged commit 817558a into main Sep 9, 2026
69 checks passed
@avrabe
avrabe deleted the feat/mvpcore-1017 branch September 9, 2026 06:55
avrabe added a commit that referenced this pull request Sep 9, 2026
…1232 landed the fix this pin was waiting for

Rebased onto main after RQ-65-MVPCORE (#1232) merged, which is the fix this
pin's own rule anticipated: "a fix makes (b) == (c), this goes red, and the fix
flips the pin (known-open -> closed) in its own PR."

RE-DERIVED on the merged tree with `--print-self-test`, not asserted. Every leg
(merged-wast, single-wast, same-text-wat):

  before   m4-reloc (7, 6, 8)   m4-self (4, 4, 8)   wast < wat, open
  after    all four legs (0, 8, 8)                  wast == wat, merged refused

Two independent routes, both now clean: the single-module `.wast` path threads
declared widths so `wast == wat`, and the multi-module merge REFUSES an
i64-param module so `merged == 0`. The assertion becomes EQUALITY plus that
refusal, which is strictly stronger than the old inequality — a narrower `wast`
OR the merge silently accepting again now goes red.

POTENCY is untouched: 192 planted writes, 0 without the plant, across 3 fixtures
x 4 legs. Self-test RESULT: PASS on the rebased tree.

Artifact prose corrected where it had become FALSE rather than merely dated:
"#1226 stays OPEN" and "not fixed here" were true when written and are not now.
The spec-suite functions the artifact said were "NOT fixed by #1222 and stay so
until #1226 is fixed" ARE now correct, because #1226 is fixed. What remains of
#1226 is a REACH note, not a correctness one: the merge refuses i64-param
modules it previously accepted and mis-compiled — decline-honesty with the
acceptance cost stated.

Emulation floor re-derived on the rebased tree: 346751, unchanged.

Refs #1189, #1226, #1232

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 Sep 9, 2026
…floors after #1232; the population fell because modules are now REFUSED

CI on the rebased tree reported the pin flip working — `WIDTH-1226: closed`,
`POTENCY: PASS (192 planted writes, 0 without the plant)`, `hits: 0` — and then
failed on the population floors, correctly:

  VACUOUS — declared floor stdout >= 8317, measured 7191

That is the floor doing its job. The cause is not scan rot: RQ-65-MVPCORE
(#1232) merged first and makes the multi-module `.wast` merge REFUSE modules it
would mis-handle (i64 params, data segments, accessed globals) instead of
silently mis-compiling them. A refused module audits no functions, so this
oracle's corpus shrank with them:

  modules compiled   1045 -> 940     (-105 refused)
  functions audited  8317 -> 7191    (-13.5 %)
  homes watched      8776 -> 8100
  attributed instrs  261440 -> 219807

Re-derived from CI's own run on the merged tree, not computed. The floors move
DOWN exactly once, here, with that reason written beside them in the script, the
artifact and ci.yml; a further fall without a reason is scan rot and stays red.

This is worth stating plainly rather than quietly re-pinning: it is the
ACCEPTANCE COST of decline-honesty, measured. #1232 traded reach for
correctness, and one place the bill lands is that a correctness oracle now
watches 13.5 % fewer functions.

Emulation floor unaffected (this oracle declares `stdout`, not `emulations`) and
re-derived at 346751 on the rebased tree.

Refs #1189, #1226, #1232

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 Sep 9, 2026
…me-register consumer on four legs; fix aarch64 rotl (#1221) and i64-param local.set (#1222) red-first; pin #1223/#1226 open (#1227)

* fix(selector): RQ-65-ALIASCLASS (#1189) — enumerate every uncopied home-register consumer on four legs; fix aarch64 rotl (#1221) and i64-param local.set (#1222) red-first; pin #1223 open

THE QUESTION. `local.get` of a register-homed local pushes THE HOME
REGISTER uncopied onto the operand stack; #677, #989 and #1189 were three
consumers that wrote it, each found as a silent wrong answer and fixed at
its own site. How many others exist, across arm-reloc, arm-self, RV32 and
AArch64? Answered mechanically, not by re-reading arms.

STATIC AUDIT (ARM direct selector). `synth_synthesis::home_alias`: an
exhaustive, wildcard-free register-writes table for all 222 ArmOp variants
(`gp_defs`/`vfp_defs`, #946-style source-scan pin) + a loop-back-edge-
extended liveness walk independent of the selector's own `param_last_read`,
hooked at the tail of `select_with_stack` under SYNTH_HOME_ALIAS_AUDIT. A
write to a live local's home that is not the local's own set/tee, and not
proven straight-line into an inline epilogue ON THE STREAM, is a loud
decline naming op, instruction, register and local. Byte-invisible
otherwise. Swept over the pinned spec suite + local corpus on {reloc,
self} x {cortex-m4, cortex-m4f}: 4628 functions, 2372 homes, 227150
attributed instructions, 0 unexplained hits; 16 hits pinned KNOWN-OPEN
against #1226 (home_alias_audit_corpus_1189.py, CI job
home-alias-audit-1189). Potency against the real history: built at the
pre-#1190 commit it flags exactly the 10 functions #1190 recorded wrong
and none of its 7 clean ones.

EXECUTION ORACLE (four legs). home_alias_class_1189_differential.py + 3
generated modules: 117 consumer-family functions re-reading their homes
after every op, expected values from wasmtime, declines pinned EXACTLY per
(module, leg), main's own objects kept as a permanent red half (10 pinned
wrong vectors). Red-first on main af81978: 990 matched / 9 diverged:
  #1221 aarch64 i32/i64.rotl negates the count IN PLACE — the #776 fix's
        "now-dead register" is a param home in a leaf function; every
        result exactly rotl + a - b (7 vectors).
  #1222 direct selector local.set/tee of a register-homed i64 PARAM writes
        only the lo half, at index_to_reg(i) not the AAPCS pair, with the
        #989 snapshot reserving only val_lo; every leaf function assigning
        an i64 param (spec suite: fac-opt, loop `while`, type-param-i64,
        the 64-bit bulk-memory checkRanges) was silently wrong.
  #1223 optimized path folds (x - x) + x to 0 — an IR defect, NOT this
        class; filed, pinned KNOWN_OPEN (the recorded wrong value must
        persist until the fix flips the pin).
  #1226 surfaced BY the #1222 fix in the audit sweep: the multi-module
        .wast merge hands a later module's function the representative
        module's param-width table (#1168 re-threads arg counts, not
        width masks), so an i64 param is homed as i32 and its pair
        overlaps the next param. A CLI-merge defect, NOT this class;
        filed, 4 spec-suite functions x 4 legs pinned exactly until fixed.
Green on the fixes: 999 matched / 0 diverged, 10/10 pinned wrong vectors
still reproduce from main's bytes, 1629 emulations. CI job
home-alias-class-1189-oracle; emulation floor 325121 -> 326750 (re-derived
by oracle_wiring_check.py; ci.yml and claims.yaml in lockstep).

FIXES, one site each. aarch64 rotl: a param count is negated into a
scratch chosen with n, k and dst held live; a temp count keeps the #776
bytes. ARM direct: `write_i64_param_home` moves both halves at the
`local_to_reg` home, ordered for partial overlap, both halves of val
reserved for the alias snapshot. Byte-identity pre vs post over 1419
compiles / 10725 functions (arm-reloc, arm-self, aarch64): 58 functions
changed, every one an aarch64 rotl function or an ARM i64-param set/tee;
0 newly declined / accepted, no skip-set moved.

STRUCTURAL PINS for the clean legs, as tests: RV32 params COPY on
local.get (the accident of implementation the artifact names) and a
promoted s8 is written only by its own set + the epilogue; AArch64 TEMPS
disjoint from x0..x7, 21 families never write a param home; the optimized
path's local vregs are never freed (18 families walked with gp_defs).

LEDGER. selector_lines_code 19740 -> 19917 by waiver (the class had no
handling to delete); tracked totals 30565 -> 30857, 91 -> 93 (two
`_ => None` in the new test). RQ-65-ALIASCLASS implemented; enumeration in
docs/analysis/HOME_REGISTER_ALIAS_CLASS_1189.md; CHANGELOG [Unreleased].

Refs #1189, #1221, #1222, #1223, #1226

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

* fix(selector): RQ-65-ALIASCLASS (#1222) — gate the i64 pair-write on the DECLARED width; the fuzz harness was right and the fix over-applied

Salvaged and completed by the coordinator after the lane hit a session rate
limit mid-diagnosis. Three CI reds on #1227, all found only after the rebase.

RED 3 — the substantive one, and the fuzz target was CORRECT.

`i64_lowering_doesnt_clobber_params` asserted that nothing may write R1 while
`LocalGet(1)` still reads it, and caught:

  Mov { rd: R0, op2: Reg(R4) }, Mov { rd: R1, op2: Reg(R5) }

The convenient reading was that the harness encoded the pre-#1222 register
model and should move with the fix. It did not. `i64_locals` carries
body-INFERRED widths (`infer_i64_locals`), not only declared ones — so a param
the signature calls i32, or whose width nobody declared (a direct
`select_with_stack` caller, #1226's stale multi-module width table, invalid
wasm), is homed NARROW by `aapcs_param_layout` and its "hi half" IS the next
param's home. The #1222 pair-write fired there and genuinely clobbered local 1.

So the fix was over-applied, not the harness stale. `declared_wide_params`
now gates the pair arm on the declared width AND the layout having homed it
as a pair; anything else keeps the single-move arm — yesterday's bytes.

Regression test covers both configurations, which is the discrimination that
was missing: undeclared widths write R0 only with nothing touching R1 before
`local.get 1`; declared `(i64 i32)` writes the pair R0:R1 with local 1 in R2.

RED 1 — the audit oracle ran VACUOUSLY in CI. `ROOT` was
`Path(__file__).parent.parent`, which from `scripts/repro/` resolves to
`scripts/`, so it looked for the suite at `scripts/tests/spec-testsuite`,
found nothing and exited 1 having audited 0 functions. Now
`.parent.parent.parent`, matching the working sibling
`selector_parity_197_differential.py` in the same directory. Its FATAL message
also stops guessing ("submodule not checked out?" — it was) and prints the
path it actually looked at plus whether that path exists.

RED 2 — `arm_corpus_sweep_973.py` auto-sweeps every `scripts/repro/*.wat`, so
the new i64 fixture joined the corpus and legitimately declines its 10 `c_*`
exports (i64 compare feeding `i64.extend_i32_u`, #952). Registered in
EXPECTED_DECLINES with that reason; the other 38 exports compile and execute
equal to wasmtime.

Verified: fmt, clippy -D warnings clean, synth-synthesis 760 + suites 0
failed, exact-emulation-floor re-derived 346751 on the rebased tree,
status_evidence and claim_check green.

Refs #1189, #1221, #1222, #1226

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

* fix(gate): RQ-65-ALIASCLASS (#1222) — move the selector ratchet and regenerate status.json for the declared-width guard

The previous commit changed select_with_stack.rs without moving the pins, and
I pushed it because my `&&` chain guarded the `echo`, not the `git push` —
the exact "captured the exit code, didn't gate" trap the standing rules name.
The gate said FAIL and the push ran anyway as a separate statement. This
commit is chained through to the push so it cannot repeat.

Two real failures behind that FAIL, both re-derived, never hand-computed:

  selector_lines_code   19917 -> 19945  ceiling, must FALL -> waived
  selector_lines_total  30857 -> 30943  tracked, no direction -> value only
  artifacts/status.json                 stale -> regenerated with --emit-status

The waiver is bound to 19945, so a further regression needs its own waiver.
Its reason records that this +28 is a CORRECTION to the same artifact's own
+177, not new capability: the #1222 pair-write was gated on `i64_locals`,
which carries body-inferred widths, so it fired on params homed narrow and
clobbered the next param's home. Narrowing the arm cannot be done by
deleting — the correct behaviour needs the declared-width test the original
arm lacked.

Refs #1189, #1222

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

* fix(oracle): RQ-65-ALIASCLASS (#1189) — re-pin the home-register audit to the REAL corpus; the 12 first-full-corpus hits were the walk, fixed in the walk

The corpus sweep's first run WITH the suite submodule and the corrected
ROOT (previous lane: suite-only, 4628 functions) read 8317 functions /
8776 homes and `hits: 12 (unexplained: 12)` plus 16 pinned #1226 hits that
did not occur — RESULT: FAIL (3). Classified one by one, nothing silenced:

SHAPE A (8) — AUDIT FALSE POSITIVE, walk fixed. `Pop { regs: [R4..R8, PC] }`
at a mid-function `Return` (cabi_arena_{bind,realloc}.wat func_0, m4/m4f
self) and at a function-level `BrIf(0)` (home_alias_class_1189_promo.wat
p_brif, 4 legs), "writes R4 = home of local N, read again at op M". The
inline epilogue restores a promoted local's r4 and RETURNS in the same
instruction, so op M is unreachable from it. `write_is_terminal` scanned
only PAST the writer (next instruction = another op's, or the br_if's own
skip label) and never asked whether the writer IS the terminator. It does
now; `Pop` has no conditional form in `ArmOp` and both encoders emit it
unconditionally. The existing terminal test planted the home at R0, never
at the register the pop restores — that is why the unit test did not see
it; the new test plants R4.

SHAPE B (4) — LEGITIMATE-BUT-SAFE, proven on the stream instead of pinned.
`F32Load { sd: S0, addr: [SP, #24|#28] }` at op `Call(11)` (f32_ops_719.wat
xhome/xcall2, m4f-reloc/-self): `vstr s0, [sp, #0x18] … bl … vldr s0, [sp,
#0x18]` — the caller-save RESTORE of the f32 home from the slot the same op
saved it to. The audit had not stated the AAPCS VFP caller-saved clobber
(s0–s15) on calls, so the `bl` that makes the restore necessary was
INVISIBLE and its restore surfaced as the write; a MISSING restore was
invisible too. `vfp_defs` now lists s0–s15 on Bl/Blx/Call/CallIndirect
(re-swept with it stated: 0 new hits — every VFP home live across a call
on this corpus is saved and restored), and `write_is_bracketed_by_save_
restore` discharges the round trip only when save and restore name the
identical register and static slot, the save precedes every other write of
the home in the op, and nothing between them touches the slot, its base or
control flow. Unit-tested: the exempt shape (incl. an argument marshalled
INTO the home between save and restore) and 9 broken variants — no
restore, other slot, overlapping store, disjoint store (fine), SP move,
stale save, split op, label — as still-flagged. Executed: the CI-wired
f32_ops_719_differential.py runs xhome/xcall2 on m4f under unicorn,
238/238 bit-exact vs wasmtime.

Each exemption's negative-control build reproduces exactly its own shape
and nothing else: pop check off -> the 8 pop hits, 0 other; round trip off
(clobber stated) -> the 4 VFP functions, now reported at the `Bl`, 0 other.
The fixed walk sweeps the IDENTICAL population (1045 pairs / 8317 / 8776 /
261440 attributed) to 0 hits.

SHAPE C — the 16 #1226 pins stopped occurring because `declared_wide_params`
(c14f53a) reads the SAME stale width table #1226 describes, so on
`checkRange` the pair-write no longer fires and nothing lands on `$to`'s
R1. Verified on the bytes: the gate removed the HIT, not the MISCOMPILE —
`checkRange` still compares `$to` at R1:R2 (`cmp r0, r1; it eq; cmpeq r1,
r2`; AAPCS passes it in R2:R3), `local.set $from` is back to the
single-half `mov r0, r5` (the `adc r6` hi half dropped), and the `i64.const
-1` return sets R0 only. #1226 stays OPEN (bytes on the issue); the pins
are removed here because no sweep line can see the defect any more, and
the script says so.

Pins re-derived on the rebased tree with the submodule present (never
carried textually): in-script FLOORS 1045/8317/8776/261440, the `ci-checks:
stdout` header >= 8317, ci.yml greps `functions audited: 8317`, `homes
watched: 8776`, `hits: 0 (known-open: 0 [none], unexplained: 0)`; exact
emulation floor re-derived by oracle_wiring_check.py: 346751, unchanged;
selector ratchets unchanged (home_alias.rs is outside the ceiling;
claim_check --metric 63/63, status.json no drift). Artifact verified-by,
docs/analysis table (the VFP call-clobber row was WRONG: "no home exists
to alias" is true of core registers only), CHANGELOG and the waiver prose
now state what was measured.

Verified: cargo fmt --check, clippy --workspace --all-targets -D warnings,
synth-synthesis 766 + suites 0 failed, CI-form sweep via oracle_run with
the four exact greps ALL MATCH, home_alias_class_1189_differential.py 999
matched / 0 diverged / 1629 emulations PASS, f32_ops_719 238/238,
status_evidence_check green, duplicate-key-strict YAML parse of ci.yml,
the artifact and claims.yaml.

Refs #1189, #1222, #1226, #1227

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

* fix(oracle): RQ-65-ALIASCLASS (#1189) — wire the audit's potency (plant probe, 192 exact) and pin #1226 red-first on its REAL mechanism: the .wast path threads no width tables

Coordinator review of #1227: the sweep's `hits: 0` proved the sweep RAN
(work floors), not that the DETECTOR FIRES — and after the walk was
relaxed to clear 12 false positives that was the property under
suspicion. Two additions, and one correction the second one forced.

POTENCY, wired. `SYNTH_HOME_ALIAS_AUDIT=verbose,plant` makes
`home_alias::plant_probe` plant ONE synthetic write of a watched home at
op 0 of every function that has a home still read later (`mov <home>,
r12` / `F32Const` into the S-slot, on a COPY of the stream; shipped bytes
untouched). The corpus script's self-test phase runs first and requires,
per (fixture, leg), EXACTLY the pinned number of declines carrying the
needle at `op 0` — i32 params r0-r3 58/7/58/7, promoted locals r4-r8
11/9/11/9, VFP homes 0/0/11/11 (soft-float m4 has none) — and 0 declines
without the plant: 192 planted writes reported, through the same decline
/ needle / parse path the sweep relies on. ci.yml greps `POTENCY: PASS
(192 …)` exactly. (I first wrote 172 by hand; the script's derived total
is 192 — the pin is the derived number.) The selector's audit hook
collapsed to one call into `home_alias::selector_hook` (63 -> 12 lines;
`selector_lines_code` 19945 -> 19896, a FALL, waiver bound to the value
as the engine requires; `selector_lines_total` 30943 -> 30894).

#1226, STATED PLAINLY — and corrected. The walk is correct and #1226 was
never a home-register WRITE: it is a mis-HOMING, which this audit cannot
see because it trusts the selector's home table by construction; the 16
hits it carried were the #1222 pair-write (fired by body inference)
landing in the neighbour's wrongly assigned R1, removed by the
declared-width gate changing the EMISSION, not the walk — no coverage of
home writes was lost. Trying to pin it exposed that the issue's mechanism
is wrong: the two-module repro MERGED and the same functions STANDALONE
as `.wast` are equally wrong; the discriminator is the extension. The
synth-cli `.wast` driver path returns `Vec::new()` for EVERY declared-
width table (func/type ret_i64, params_i64/_f32/_f64, the f32/f64 return
masks — each annotated "WAST fixture suite is i32-only"), so on ANY
`.wast`, single-module too, an i64/f32/f64 param or result whose width
body inference cannot recover is homed as i32. Measured: the same module
text as `.wat` gives `second3` homes=4 and `mov r0, r2; mov r1, r3`; as a
`.wast` homes=2 and `mov r0, r1`; `fac-opt` is rescued by inference.
Static upper bound: 1393 exported spec-suite functions in 74 files carry
a wide param or result. Pinned RED-FIRST in the same self-test: one
fixture (`home_alias_width_1226.wast`), its last module derived at run
time as a single `.wast` and as the identical `.wat`; `homes=` must be
`wast < wat` on every leg — (7, 6, 8) relocatable, (4, 4, 8) self-
contained; ci.yml greps `WIDTH-1226: known-open (... on 4 legs)`; a fix
makes them equal and must flip the pin.

CONSEQUENCE for #1222, recorded in CHANGELOG / artifact / docs: the
pair-write is gated on the DECLARED width, so on `.wast` input it never
fires — the spec-suite functions the #1222 entry named as fixed (fac-opt,
loop `while`, type-param-i64, the 64-bit bulk-memory `checkRange`s) are
NOT fixed; they are wrong for #1226's reason until #1226 is fixed. #1222
holds for `.wat`/`.wasm` input (its own fixtures, executed).

Verified: fmt --check, clippy --workspace --all-targets -D warnings,
synth-synthesis 766 + suites 0 failed (16 home_alias unit tests incl.
the probe and S_REGS inverse), CI-form sweep via oracle_run with all SIX
exact greps matching (POTENCY 192, WIDTH-1226 known-open, 8317 / 8776 /
hits 0 / RESULT: PASS), exact emulation floor re-derived 346751
(unchanged: the self-test is stdout-declared), claim_check 63/63 with
status.json regenerated, status_evidence_check green, duplicate-key-
strict YAML on ci.yml / artifact / claims.

Refs #1189, #1222, #1226, #1227

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

* fix(oracle): RQ-65-ALIASCLASS (#1226) — flip the width pin to CLOSED; #1232 landed the fix this pin was waiting for

Rebased onto main after RQ-65-MVPCORE (#1232) merged, which is the fix this
pin's own rule anticipated: "a fix makes (b) == (c), this goes red, and the fix
flips the pin (known-open -> closed) in its own PR."

RE-DERIVED on the merged tree with `--print-self-test`, not asserted. Every leg
(merged-wast, single-wast, same-text-wat):

  before   m4-reloc (7, 6, 8)   m4-self (4, 4, 8)   wast < wat, open
  after    all four legs (0, 8, 8)                  wast == wat, merged refused

Two independent routes, both now clean: the single-module `.wast` path threads
declared widths so `wast == wat`, and the multi-module merge REFUSES an
i64-param module so `merged == 0`. The assertion becomes EQUALITY plus that
refusal, which is strictly stronger than the old inequality — a narrower `wast`
OR the merge silently accepting again now goes red.

POTENCY is untouched: 192 planted writes, 0 without the plant, across 3 fixtures
x 4 legs. Self-test RESULT: PASS on the rebased tree.

Artifact prose corrected where it had become FALSE rather than merely dated:
"#1226 stays OPEN" and "not fixed here" were true when written and are not now.
The spec-suite functions the artifact said were "NOT fixed by #1222 and stay so
until #1226 is fixed" ARE now correct, because #1226 is fixed. What remains of
#1226 is a REACH note, not a correctness one: the merge refuses i64-param
modules it previously accepted and mis-compiled — decline-honesty with the
acceptance cost stated.

Emulation floor re-derived on the rebased tree: 346751, unchanged.

Refs #1189, #1226, #1232

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

* fix(oracle): RQ-65-ALIASCLASS (#1189) — re-derive the audit's corpus floors after #1232; the population fell because modules are now REFUSED

CI on the rebased tree reported the pin flip working — `WIDTH-1226: closed`,
`POTENCY: PASS (192 planted writes, 0 without the plant)`, `hits: 0` — and then
failed on the population floors, correctly:

  VACUOUS — declared floor stdout >= 8317, measured 7191

That is the floor doing its job. The cause is not scan rot: RQ-65-MVPCORE
(#1232) merged first and makes the multi-module `.wast` merge REFUSE modules it
would mis-handle (i64 params, data segments, accessed globals) instead of
silently mis-compiling them. A refused module audits no functions, so this
oracle's corpus shrank with them:

  modules compiled   1045 -> 940     (-105 refused)
  functions audited  8317 -> 7191    (-13.5 %)
  homes watched      8776 -> 8100
  attributed instrs  261440 -> 219807

Re-derived from CI's own run on the merged tree, not computed. The floors move
DOWN exactly once, here, with that reason written beside them in the script, the
artifact and ci.yml; a further fall without a reason is scan rot and stays red.

This is worth stating plainly rather than quietly re-pinning: it is the
ACCEPTANCE COST of decline-honesty, measured. #1232 traded reach for
correctness, and one place the bill lands is that a correctness oracle now
watches 13.5 % fewer functions.

Emulation floor unaffected (this oracle declares `stdout`, not `emulations`) and
re-derived at 346751 on the rebased tree.

Refs #1189, #1226, #1232

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 Sep 9, 2026
… introduced instead of carrying it

Rebased onto main after RQ-65-ALIASCLASS (#1227) and RQ-65-MVPCORE (#1232).
ci.yml conflicted because both PRs inserted a job at the same point; resolved by
taking main's file whole and re-inserting the mutation job after the
home-alias-audit job, then verifying BOTH parse and are present (64 jobs).
CHANGELOG kept one `[Unreleased]` header with both bodies.

MEASURED on the rebased tree, not assumed. #1232 grew
crates/synth-cli/src/main.rs 11143 -> 11405 and mutation sites are anchored by
file:line:col:

  sites enumerated                     1542
  ci_subset entries still resolving    7 of 7   (3 controls + 4 mutants)
  R5-startup mutants still resolving   0 of 7

THE LIVE GATE IS INTACT, and for a structural reason worth recording: the three
controls come from `control_sites()` rather than a line anchor, and the four
subset mutants live in optimizer_bridge.rs / liveness.rs / arm_backend.rs, which
#1232 did not touch. `mutation-survey-discrimination` still replays real
mutations and still fails if a recorded killer stops killing.

WHAT IS STALE, said in the report rather than fixed silently: all seven
R5-startup mutants point at moved code. They are 7 of the 21 byte-changing
mutants the rate is computed over (6 KILLED, 1 UNTESTED) — and that one UNTESTED
is one of the FOUR published survivors. So the 19 % stands as a measurement of
the tree it names, a third of its denominator is not replayable on main, and
re-anchoring R5 is follow-up work rather than a re-run: #1232 restructured that
region, so "the equivalent site" is a judgement, not a line-number shift.

A survey whose denominator quietly stops being checkable is the failure this
release exists to name. Recording it is the point.

Floor re-derived on the rebased tree: 346751, unchanged (this job declares no
emulations). Ratchets unmoved; mutants_untested 4 = pinned.

Refs #1189, #1232, #1227

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 Sep 9, 2026
…hat drifted; the baseline moved, not the site

CI failed the survey's own replay with `changed-set moved: 53 vs ledger 49`. That
is the gate working: all three controls still died to their recorded killers, both
the EQUIVALENT and DEAD entries reproduced, and exactly one entry disagreed.

CAUSE, measured rather than guessed. The site did not move. The BASELINE did:
13 of 609 corpus entries differ on the merged tree after #1232 and #1227, so the
mutation's blast radius moved with them. Byte triage re-derived on the merged tree
gives 53, and CI and a local run agree at 53 independently. Applied to both the
mutant record and the ci_subset entry.

`reanchor` separately relocated all 15 line-shifted sites by their stored `before`
TEXT (0 not found), so last commit's "R5 mutants are not replayable" is obsolete
and the report section is rewritten with the measured figures.

NOT RE-VERIFIED, AND THE REPORT SAYS SO: that mutant's CLASSIFICATION was measured
at `meta.commit` and is not re-established here. The CI replay checks byte triage
for UNTESTED entries, not the oracle suite (that needs --full). The published rate
stays relative to the commit the ledger names.

A LOCAL FULL RE-SURVEY WAS ATTEMPTED AND DISCARDED AS INVALID, recorded because
the near-miss matters: 147 of the 196 L1 steps invoke bare `python`, absent on
this machine, so they exit 127 in 0.1s and the harness scores that as a KILL; and
`fact_spec_div_494_differential.py` is red on the UNMUTATED tree here while green
in CI. That run reported 0 of 29 surviving — dead oracles, not a result. The
harness's own control check refused it: "1 control(s) not killed — the survey
measures nothing; do not publish the rate". A gate that stops its author
publishing a flattering number is the one worth having.

Refs #1189, #1232, #1227

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 Sep 9, 2026
…changing mutants survive a named suite (19 %, upper bound); 3 controls KILLED; survivors enumerated; ledger CI-pinned (#1233)

* feat(mutants): RQ-65-MUTANTS (#1189) — mutation survey: 4 of 21 byte-changing mutants survive a named 5-job suite + workspace tests (19 %, upper bound); 3 red-first controls KILLED; survivors enumerated; ledger CI-pinned

The v0.65 headline number, measured rather than asserted. scripts/mutation_survey.py
flips one codegen decision at a time (REG / COND / DROPMOV / IMM / GUARD / BOUND)
over five anchor-located regions supplied by RQ-65-PARITY, rebuilds, triages by
emitted bytes over 200 corpus modules x 3 ARM configurations, and runs a suite
DERIVED from ci.yml only on byte-changing mutants.

Measured at 580d53f, re-anchored at 761c692, seed 1189: 38 drawn, 6 uncompilable,
32 compiled, 21 byte-changing; 17 KILLED (6 execution, 8 structure, 2 freeze-only,
1 compiler hang), 4 UNTESTED = 19 % survival relative to the NAMED suite
(wast-conformance, repro-sweep-arm-corpus, join-alias-1189, cmp-select,
frame-slot-dce-242 + cargo test --workspace) — an upper bound under the full CI
board. 11 byte-identical: 5 EQUIVALENT, 4 DEAD, 2 UNRESOLVED (declared).
Controls: the #1189 copy disabled, the select operands swapped, the startup R10
seed into R9 — all KILLED (the third only by byte goldens: no execution oracle in
the suite boots the shipped Reset_Handler).

CI pin: the mutation-survey-discrimination job replays the ledger's ci_subset
(3 controls + 4 non-killed) and fails in either direction; claims.yaml gains the
eighth kind: ratchet (mutants_untested, a ceiling over docs/status/mutation_survey.json)
plus count-eq pins on 38 / 21 / 4, the controls and the subset floors. No new
emulation declaration; the exact floor is untouched.

Refs #1189

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

* docs(mutants): RQ-65-MUTANTS (#1189) — record PR #1233 in the artifact's landed field

Refs #1189

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

* docs(mutants): RQ-65-MUTANTS (#1189) — state the ledger staleness #1232 introduced instead of carrying it

Rebased onto main after RQ-65-ALIASCLASS (#1227) and RQ-65-MVPCORE (#1232).
ci.yml conflicted because both PRs inserted a job at the same point; resolved by
taking main's file whole and re-inserting the mutation job after the
home-alias-audit job, then verifying BOTH parse and are present (64 jobs).
CHANGELOG kept one `[Unreleased]` header with both bodies.

MEASURED on the rebased tree, not assumed. #1232 grew
crates/synth-cli/src/main.rs 11143 -> 11405 and mutation sites are anchored by
file:line:col:

  sites enumerated                     1542
  ci_subset entries still resolving    7 of 7   (3 controls + 4 mutants)
  R5-startup mutants still resolving   0 of 7

THE LIVE GATE IS INTACT, and for a structural reason worth recording: the three
controls come from `control_sites()` rather than a line anchor, and the four
subset mutants live in optimizer_bridge.rs / liveness.rs / arm_backend.rs, which
#1232 did not touch. `mutation-survey-discrimination` still replays real
mutations and still fails if a recorded killer stops killing.

WHAT IS STALE, said in the report rather than fixed silently: all seven
R5-startup mutants point at moved code. They are 7 of the 21 byte-changing
mutants the rate is computed over (6 KILLED, 1 UNTESTED) — and that one UNTESTED
is one of the FOUR published survivors. So the 19 % stands as a measurement of
the tree it names, a third of its denominator is not replayable on main, and
re-anchoring R5 is follow-up work rather than a re-run: #1232 restructured that
region, so "the equivalent site" is a judgement, not a line-number shift.

A survey whose denominator quietly stops being checkable is the failure this
release exists to name. Recording it is the point.

Floor re-derived on the rebased tree: 346751, unchanged (this job declares no
emulations). Ratchets unmoved; mutants_untested 4 = pinned.

Refs #1189, #1232, #1227

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

* fix(mutants): RQ-65-MUTANTS (#1189) — re-derive the one changed-set that drifted; the baseline moved, not the site

CI failed the survey's own replay with `changed-set moved: 53 vs ledger 49`. That
is the gate working: all three controls still died to their recorded killers, both
the EQUIVALENT and DEAD entries reproduced, and exactly one entry disagreed.

CAUSE, measured rather than guessed. The site did not move. The BASELINE did:
13 of 609 corpus entries differ on the merged tree after #1232 and #1227, so the
mutation's blast radius moved with them. Byte triage re-derived on the merged tree
gives 53, and CI and a local run agree at 53 independently. Applied to both the
mutant record and the ci_subset entry.

`reanchor` separately relocated all 15 line-shifted sites by their stored `before`
TEXT (0 not found), so last commit's "R5 mutants are not replayable" is obsolete
and the report section is rewritten with the measured figures.

NOT RE-VERIFIED, AND THE REPORT SAYS SO: that mutant's CLASSIFICATION was measured
at `meta.commit` and is not re-established here. The CI replay checks byte triage
for UNTESTED entries, not the oracle suite (that needs --full). The published rate
stays relative to the commit the ledger names.

A LOCAL FULL RE-SURVEY WAS ATTEMPTED AND DISCARDED AS INVALID, recorded because
the near-miss matters: 147 of the 196 L1 steps invoke bare `python`, absent on
this machine, so they exit 127 in 0.1s and the harness scores that as a KILL; and
`fact_spec_div_494_differential.py` is red on the UNMUTATED tree here while green
in CI. That run reported 0 of 29 surviving — dead oracles, not a result. The
harness's own control check refused it: "1 control(s) not killed — the survey
measures nothing; do not publish the rate". A gate that stops its author
publishing a flattering number is the one worth having.

Refs #1189, #1232, #1227

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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