From e88833e151a0fceb97785f8cabd3e2f1b3530529 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sat, 27 Jun 2026 07:23:22 +0200 Subject: [PATCH] test(vcr-oracle): broaden #518 oracle to the full AAPCS i64-param matrix (#242, #518) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No new external signal this issue-hunt pass (gates #418/#472/#396-emitter still await external triggers; no new issues/comments since watermark; deps unchanged). Bounded behavior-frozen #242 increment: harden the #518 characterization oracle (landed #525) to cover the AAPCS register-assignment cases the gated fix's mapping logic must handle but the single-param fixture missed. Measured (objdump force-thumb + unicorn vs wasmtime) — distinct failure modes the sequential `index_to_reg(i)` param mapping produces beyond the single-param clobber: - (i64,i64): p1 is the even-aligned pair R2:R3 but sequential mapping puts it at R1, so `i64.add(p0,p1)` emits `adds r3,r0,r1` = p0.lo+p0.hi (not p0+p1). - (i64,i32): the trailing i32 param (R2) is ignored entirely. - reading only the SECOND i64 param: sequential reads p1 from R1 (wrong) AND the i64.const clobbers R2 (the real p1.lo). Fixture grows by t_ii_add / t_i64_i32 / t_snd_i64; the differential gains those cases (AAPCS-correct arg placement already handles even-aligned pairs) and an explicit i32-control-regression guard (a broken control now FAILS the characterization rather than being masked by the divergence count). Result: 10/10 i64-param functions diverge on BOTH paths; i32 control correct on both. FROZEN-SAFE — no codegen touched (frozen anchors 3/3 green); rivet clean. The byte-changing fix stays a separate gated step. Co-Authored-By: Claude Opus 4.8 --- artifacts/verified-codegen-roadmap.yaml | 10 ++++-- scripts/repro/i64_param_518.wat | 13 ++++++++ scripts/repro/i64_param_518_differential.py | 36 ++++++++++++++------- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/artifacts/verified-codegen-roadmap.yaml b/artifacts/verified-codegen-roadmap.yaml index 4c440b3..62bc304 100644 --- a/artifacts/verified-codegen-roadmap.yaml +++ b/artifacts/verified-codegen-roadmap.yaml @@ -747,8 +747,14 @@ artifacts: (frozen-safe, no codegen touched): `scripts/repro/i64_param_518.wat` + `i64_param_518_differential.py` — AAPCS-correct arg placement (even-aligned i64 pairs: `(i32,i64)` → R0,R2:R3), i64-aware result readback (R0|R1<<32), - runs BOTH paths vs wasmtime. Measured: 6/6 i64-param functions diverge on - each path; the i32 control (`t_i32`) stays correct on both. The script is a + runs BOTH paths vs wasmtime. Measured: 10/10 i64-param functions diverge on + each path; the i32 control (`t_i32`) stays correct on both. The fixture + covers the FULL AAPCS i64-param register-assignment matrix the fix's mapping + logic must get right — not just the single leading `(i64)` (R0:R1) case but + the multi-param shapes the sequential `index_to_reg(i)` mapping botches: + `(i64,i64)` (p1 = even-aligned R2:R3, mis-mapped to R1 → computes p0.lo+p0.hi + instead of p0+p1), `(i64,i32)` (p1=R2 ignored), and reading ONLY the second + i64 param (sequential reads R1, and the const clobbers the real p1.lo=R2). The script is a stable characterization (exit 0 = "bug reproduces as the root cause documents"); the gated fix flips its `EXPECT_MISCOMPILE→False` and it becomes the correctness gate. BOUNDED FIX SPEC for the gated step (NOT this tick): diff --git a/scripts/repro/i64_param_518.wat b/scripts/repro/i64_param_518.wat index 2b1f106..1f8a254 100644 --- a/scripts/repro/i64_param_518.wat +++ b/scripts/repro/i64_param_518.wat @@ -16,6 +16,19 @@ ;; i64 param NOT at index 0 (after an i32) — exercises pair-shift past a scalar (func (export "t_mixed") (param i32 i64) (result i64) (i64.add (local.get 1) (i64.extend_i32_u (local.get 0)))) + ;; --- AAPCS i64-param register-assignment matrix (the fix's hardest logic) --- + ;; TWO i64 params: p0=R0:R1, p1=R2:R3 (consecutive even-aligned pairs). The + ;; sequential `index_to_reg(i)` mapping puts p1 at R1 (wrong) and computes + ;; p0.lo+p0.hi instead of p0+p1. + (func (export "t_ii_add") (param i64 i64) (result i64) + (i64.add (local.get 0) (local.get 1))) + ;; i64 then i32: p0=R0:R1, p1=R2 (no even-align padding needed for the i32). + (func (export "t_i64_i32") (param i64 i32) (result i64) + (i64.add (local.get 0) (i64.extend_i32_u (local.get 1)))) + ;; read ONLY the SECOND i64 param (p1=R2:R3) + const: sequential mapping reads + ;; p1 from R1 (wrong) AND the const clobbers R2 (the real p1.lo). + (func (export "t_snd_i64") (param i64 i64) (result i64) + (i64.add (local.get 1) (i64.const 9))) ;; control: i32 param binop must stay correct (func (export "t_i32") (param i32) (result i32) (i32.add (local.get 0) (i32.const 5)))) diff --git a/scripts/repro/i64_param_518_differential.py b/scripts/repro/i64_param_518_differential.py index 9533809..3e7062c 100644 --- a/scripts/repro/i64_param_518_differential.py +++ b/scripts/repro/i64_param_518_differential.py @@ -73,11 +73,18 @@ ("t_sub", [(100,)], "i64"), ("t_or", [(0x8,)], "i64"), ("t_mixed", [(3, 10)], "i32,i64"), + # AAPCS i64-param register-assignment matrix — the even-aligned-pair vs + # sequential-index distinction the fix's mapping logic must get right. + ("t_ii_add", [(7, 5)], "i64,i64"), # p0=R0:R1, p1=R2:R3 + ("t_ii_add", [(0xFFFFFFFF, 1)], "i64,i64"), + ("t_i64_i32", [(10, 3)], "i64,i32"), # p0=R0:R1, p1=R2 + ("t_snd_i64", [(100, 40)], "i64,i64"), # reads only p1=R2:R3 ("t_i32", [(7,)], "i32"), ] # functions whose result is i64 (read R0:R1) vs i32 (read R0 only) -I64_RESULT = {"t_add", "t_sub", "t_or", "t_mixed"} -I64_PARAM_FNS = {"t_add", "t_sub", "t_or", "t_mixed"} +I64_RESULT = {"t_add", "t_sub", "t_or", "t_mixed", + "t_ii_add", "t_i64_i32", "t_snd_i64"} +I64_PARAM_FNS = I64_RESULT def wasmtime_run(fn, args): @@ -161,11 +168,14 @@ def run_path(label, relocatable): code, base, syms = load(out) print(f"\n=== {label} path ===") divergences = 0 + control_ok = True for fn, args, sig in CASES: faddr = syms.get(fn) if faddr is None: print(f" {fn}: SYMBOL MISSING") divergences += 1 + if fn not in I64_PARAM_FNS: + control_ok = False continue exp = wasmtime_run(fn, args[0]) got = unicorn_run(code, base, faddr, args, sig, fn in I64_RESULT) @@ -173,26 +183,30 @@ def run_path(label, relocatable): flag = "ok " if match else "BUG" if not match: divergences += 1 + if fn not in I64_PARAM_FNS: # an i32-control regression is a real fail + control_ok = False print(f" [{flag}] {fn}{args[0]} -> {got} (wasmtime {exp})" - + ("" if match else f" [MISCOMPILE: off by hi/param]")) - return divergences + + ("" if match else " [MISCOMPILE: off by hi/param]")) + return divergences, control_ok def main(): - opt_div = run_path("OPTIMIZED (--target cortex-m4)", relocatable=False) - rel_div = run_path("DIRECT (--relocatable)", relocatable=True) + opt_div, opt_ctrl = run_path("OPTIMIZED (--target cortex-m4)", relocatable=False) + rel_div, rel_ctrl = run_path("DIRECT (--relocatable)", relocatable=True) print("\n--- characterization verdict ---") # The i32 control must ALWAYS match; only i64-param fns are allowed to diverge # while the bug is live. So a "healthy" buggy state = both paths diverge on - # >=1 i64-param fn AND neither diverges on t_i32. + # >=1 i64-param fn AND neither breaks the i32 control. if EXPECT_MISCOMPILE: - ok = opt_div > 0 and rel_div > 0 - print(f"opt divergences={opt_div} direct divergences={rel_div}") + ok = opt_div > 0 and rel_div > 0 and opt_ctrl and rel_ctrl + print(f"opt divergences={opt_div} (control_ok={opt_ctrl}) " + f"direct divergences={rel_div} (control_ok={rel_ctrl})") if ok: print("RESULT: PASS — #518 reproduces on BOTH paths exactly as the " - "root-cause documents (i64 param dropped/high-half clobbered; " - "i32 control correct). Oracle ready for the gated fix.") + "root-cause documents (i64 param dropped/high-half clobbered " + "across the full single/dual/even-aligned matrix; i32 control " + "correct). Oracle ready for the gated fix.") sys.exit(0) print("RESULT: FAIL — reality diverged from the documented #518 root " "cause (a path no longer miscompiles, or the i32 control broke). "