diff --git a/CHANGELOG.md b/CHANGELOG.md index c4bbe9c2..aecc06f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,16 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.50.0] - 2026-07-22 +## [0.50.1] - 2026-07-23 + +### Changed + +- **gpio-thin size regression (#846): `SYNTH_SHIFT_MASK_ELIDE` now default-on.** + The #686 shift-mask elision (dropping a redundant `and rN, #31` mask-of-shift- + amount when the amount is provably <32, incl. the `and X,#0x1f`-masked-operand + Pattern B) shipped flag-off in v0.49. gale's real 656 B `gpio.loom.wasm` emitted + 10 such redundant `and.w ip, rN, #31` sites → the +9% (490 → 534 B) regression. + Flipping the elision on by default recovers **534 → 506 B** (−28 B; 6 of the 10 + masks proven redundant). Execution proven bit-identical vs wasmtime on gale's + driver (75/75 mmio traces incl. pin ≥ 32) and the corpus sweep. Opt-out + `SYNTH_SHIFT_MASK_ELIDE=0`. The 4 residual masks (the ones the elision can't + prove <32, ~16 B to gale's 490 B target) are a tracked follow-up. + +### Fixed + +- **[0.50.0 doc correction] The v0.50.0 notes described "the SMT validator models + i64 remainder natively instead of havoc" — that lane (#844) was reverted before + the v0.50.0 tag** (the ordeal 0.12 solver-perf regression it depended on hung CI; + ordeal pinned to =0.9.1, #849). i64 remainder is modeled as havoc in 0.50.0 and + 0.50.1; the native model re-lands behind a per-query solver timeout (#848). The + 0.50.0 intro prose below is left as-shipped for the historical record; this note + is the correction. + +## [0.50.0] - 2026-07-23 **"Cross-backend verified core, and the allocator endgame begins."** The whole-function register-allocation validator is now cross-backend (ARM incl. -optimized joins + RV32), the SMT validator models i64 remainder natively instead -of havoc, and — the headline — **a from-construction graph-colouring register -allocator lands (flag-off), with every function it emits validated by that same -verified oracle.** That is the North Star's first foothold: the verified checker -gating its own replacement. Seven parallel lanes; every one oracle-gated red-first; -frozen anchors byte-identical throughout. +optimized joins + RV32) and — the headline — **a from-construction graph-colouring +register allocator lands (flag-off), with every function it emits validated by that +same verified oracle.** That is the North Star's first foothold: the verified +checker gating its own replacement. Seven parallel lanes; every one oracle-gated +red-first; frozen anchors byte-identical throughout. *(Correction: an earlier draft +of these notes claimed native i64-remainder SMT modeling; that lane was reverted +pre-tag — see the [0.50.1] Fixed note.)* ### Added diff --git a/Cargo.toml b/Cargo.toml index 5c70871a..3daa34cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ resolver = "2" # semver to publish, so the convention now catches up: workspace # version follows the release tag, bumped pre-tag in the release # checklist. See docs/release-process.md. -version = "0.50.0" +version = "0.50.1" edition = "2024" rust-version = "1.88" authors = ["PulseEngine Team"] diff --git a/MODULE.bazel b/MODULE.bazel index 937c0e5c..acbd9602 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -7,7 +7,7 @@ module( name = "synth", # Kept in lockstep with [workspace.package] version in Cargo.toml. # Both are bumped pre-tag — see docs/release-process.md. - version = "0.50.0", + version = "0.50.1", ) # Bazel dependencies diff --git a/artifacts/parity-benchmark.md b/artifacts/parity-benchmark.md index 7380663f..5663fc30 100644 --- a/artifacts/parity-benchmark.md +++ b/artifacts/parity-benchmark.md @@ -92,7 +92,7 @@ Regenerate everything on this page: | gust_poll bounds (8 sw guards) | synth AOT --safety-bounds software (no facts) | 184 | MEASURED | 1.77x | OPEN — gale silicon (DWT) | | gust_poll bounds (8 sw guards) | synth AOT sw bounds + SYNTH_FACT_SPEC=1 (proven slot∈[0,63]) | 104 | MEASURED (8 ordeal ADMITs) | 1.00x | OPEN — gale silicon (DWT) | | gust_poll bounds (8 sw guards) | synth AOT unguarded floor (no --safety-bounds) — group ref | 104 | MEASURED | ref | OPEN — gale silicon (DWT) | -| flat_flight | synth AOT (default) | 458 | MEASURED | 2.54x | OPEN — gale silicon (DWT) | +| flat_flight | synth AOT (default) | 384 | MEASURED | 2.13x | OPEN — gale silicon (DWT) | | flat_flight | native C — arm-none-eabi-gcc -Os | 180 | MEASURED | ref | OPEN — gale silicon (DWT) | | falcon_axis (f32) | synth AOT (cortex-m4f, VFP) | 72 | MEASURED | 1.64x | OPEN — gale silicon (DWT) | | falcon_axis (f32) | native C — arm-none-eabi-gcc -Os | 44 | MEASURED | ref | OPEN — gale silicon (DWT) | @@ -153,7 +153,7 @@ The gap is CLOSING release-over-release, lever by lever, each evidence-gated: Open, tracked, and honestly not yet won: general regalloc overhead (gust_poll 3.48x, spill_reload ~31%, #390/#242 Track A), flat_flight at -2.54x vs gcc, falcon f32 at +2.13x vs gcc, falcon f32 at 1.64x vs gcc (constant materialization via `movw/movt+vmov` instead of a literal pool, unused callee-save push), and every cycles cell. diff --git a/artifacts/size_attribution_390.md b/artifacts/size_attribution_390.md index 59b34f4b..329e63f7 100644 --- a/artifacts/size_attribution_390.md +++ b/artifacts/size_attribution_390.md @@ -15,11 +15,11 @@ thumbv7m baseline (gust_poll 208 B, gust_mix 12 B, 3.9x) is **CITED** from issue | function | synth .text (measured) | LLVM (cited #390) | ratio | #390 cited synth | |---|---|---|---|---| -| gust_poll | 716 B | 208 B | 3.44x | 816 B (v0.11.50) | +| gust_poll | 692 B | 208 B | 3.33x | 816 B (v0.11.50) | | gust_mix | 32 B | 12 B | 2.67x | 44 B (v0.11.50) | synth has already shrunk since #390 was filed (levers landed v0.12–v0.39): the -gap is now ~3.44x / ~2.67x, not 3.9x / 3.7x. In particular the +gap is now ~3.33x / ~2.67x, not 3.9x / 3.7x. In particular the `and #0xffff -> uxth` peephole (issue Pass 5) has landed — gust_mix now emits `uxth r3, r0`, not the `movw+and` pair. Still open: spill churn, address re-materialization, the shadow-stack dance, and bool materialization. @@ -28,10 +28,10 @@ re-materialization, the shadow-stack dance, and bool materialization. | function | .text B | spill_reload | addr_materialize | redundant_const | prologue_shadow | guard_bool | copy_move | productive | align_pad | |---|---|---|---|---|---|---|---|---|---| -| gust_poll | 716 | 196 | 56 | 62 | 36 | 108 | 66 | 192 | 0 | +| gust_poll | 692 | 196 | 56 | 58 | 36 | 96 | 66 | 184 | 0 | | gust_mix | 32 | 4 | 0 | 0 | 16 | 0 | 6 | 6 | 0 | -| func_0 | 408 | 8 | 44 | 130 | 8 | 46 | 14 | 156 | 2 | -| func_1 | 76 | 0 | 0 | 28 | 8 | 12 | 8 | 20 | 0 | +| func_0 | 380 | 8 | 44 | 138 | 8 | 26 | 14 | 142 | 0 | +| func_1 | 60 | 0 | 0 | 24 | 8 | 4 | 8 | 16 | 0 | Each instruction is assigned to exactly ONE bucket by `size_attribution_390.py::classify`; the buckets plus the `productive` residual diff --git a/artifacts/status.json b/artifacts/status.json index 5201ba03..900328a7 100644 --- a/artifacts/status.json +++ b/artifacts/status.json @@ -15,7 +15,7 @@ "sel_dsl_pilot_qed": 7, "sel_dsl_rule_qed": 50, "sel_dsl_rules": 50, - "version": "0.50.0", + "version": "0.50.1", "verus_spec_fns": 8, "wasmcert_bridge_qed": 104 } diff --git a/crates/synth-backend-aarch64/Cargo.toml b/crates/synth-backend-aarch64/Cargo.toml index e8b88f5f..b9212406 100644 --- a/crates/synth-backend-aarch64/Cargo.toml +++ b/crates/synth-backend-aarch64/Cargo.toml @@ -11,6 +11,6 @@ categories.workspace = true description = "AArch64 (A64) host-native backend for synth — integer subset (milestone 1, #538)" [dependencies] -synth-core = { path = "../synth-core", version = "0.50.0" } +synth-core = { path = "../synth-core", version = "0.50.1" } thiserror.workspace = true tracing.workspace = true diff --git a/crates/synth-backend-awsm/Cargo.toml b/crates/synth-backend-awsm/Cargo.toml index e93de7b0..2a8154d7 100644 --- a/crates/synth-backend-awsm/Cargo.toml +++ b/crates/synth-backend-awsm/Cargo.toml @@ -11,6 +11,6 @@ categories.workspace = true description = "aWsm backend integration for the Synth compiler" [dependencies] -synth-core = { path = "../synth-core", version = "0.50.0" } +synth-core = { path = "../synth-core", version = "0.50.1" } anyhow.workspace = true thiserror.workspace = true diff --git a/crates/synth-backend-riscv/Cargo.toml b/crates/synth-backend-riscv/Cargo.toml index bf6ee04c..f6ffe396 100644 --- a/crates/synth-backend-riscv/Cargo.toml +++ b/crates/synth-backend-riscv/Cargo.toml @@ -11,8 +11,8 @@ categories.workspace = true description = "RISC-V encoder, ELF builder, PMP allocator, and bare-metal startup for synth" [dependencies] -synth-core = { path = "../synth-core", version = "0.50.0" } -synth-synthesis = { path = "../synth-synthesis", version = "0.50.0" } +synth-core = { path = "../synth-core", version = "0.50.1" } +synth-synthesis = { path = "../synth-synthesis", version = "0.50.1" } anyhow.workspace = true thiserror.workspace = true tracing.workspace = true diff --git a/crates/synth-backend-wasker/Cargo.toml b/crates/synth-backend-wasker/Cargo.toml index d2bfd5fa..78c8f6da 100644 --- a/crates/synth-backend-wasker/Cargo.toml +++ b/crates/synth-backend-wasker/Cargo.toml @@ -11,6 +11,6 @@ categories.workspace = true description = "Wasker backend integration for the Synth compiler" [dependencies] -synth-core = { path = "../synth-core", version = "0.50.0" } +synth-core = { path = "../synth-core", version = "0.50.1" } anyhow.workspace = true thiserror.workspace = true diff --git a/crates/synth-backend/Cargo.toml b/crates/synth-backend/Cargo.toml index 11086a4b..3d031304 100644 --- a/crates/synth-backend/Cargo.toml +++ b/crates/synth-backend/Cargo.toml @@ -15,8 +15,8 @@ default = ["arm-cortex-m"] arm-cortex-m = ["synth-synthesis"] [dependencies] -synth-core = { path = "../synth-core", version = "0.50.0" } -synth-synthesis = { path = "../synth-synthesis", version = "0.50.0", optional = true } +synth-core = { path = "../synth-core", version = "0.50.1" } +synth-synthesis = { path = "../synth-synthesis", version = "0.50.1", optional = true } anyhow.workspace = true thiserror.workspace = true @@ -24,4 +24,4 @@ thiserror.workspace = true # #667 move 2: the i64 pseudo-op expansion certification oracle # (tests/i64_expansion_certification.rs) feeds THIS crate's emitted encoder # bytes to the synth-verify expansion validator. Dev-only — no prod-dep edge. -synth-verify = { path = "../synth-verify", version = "0.50.0", features = ["arm"] } +synth-verify = { path = "../synth-verify", version = "0.50.1", features = ["arm"] } diff --git a/crates/synth-backend/src/arm_backend.rs b/crates/synth-backend/src/arm_backend.rs index f28c9e64..ff97a25e 100644 --- a/crates/synth-backend/src/arm_backend.rs +++ b/crates/synth-backend/src/arm_backend.rs @@ -1257,14 +1257,23 @@ fn compile_wasm_to_arm( // shift) and before branch resolution (removal/rewrite-only ⇒ // offset-neutral). // - // FLAG-OFF (opt-in via `SYNTH_SHIFT_MASK_ELIDE=1`) because the elision - // moves the frozen anchors: const-amount shifts in control_step (−20 B), - // flight_seam (−164 B) and flight_seam_flat (−168 B) fold back to the - // immediate form — byte-shapes the corpus had BEFORE the #682 mask, now - // with the mask soundly kept for every unproven amount. Flipping - // default-on is a deliberate byte-changing refreeze (all differentials - // re-run on the new bytes, goldens re-pinned) owned by the maintainer. - let arm_instrs = if std::env::var("SYNTH_SHIFT_MASK_ELIDE").is_ok_and(|v| v != "0") { + // DEFAULT-ON since v0.50.1 (opt-out via `SYNTH_SHIFT_MASK_ELIDE=0`; #846). + // gale's gpio-thin driver regressed +44 B / +9% on synth 0.49 — its pin + // bit-arithmetic (`pin & 31` then a register shift) emits the source + // `and rN,#0x1f` IMMEDIATELY followed by the #682 mod-32 re-mask + // `and r12,rN,#0x1f`; the second is provably redundant (Pattern B: an + // operand produced by `and X,#c`, c<32, is already in [0,31]), so the + // pass drops it. Flipping default-on is a deliberate byte-changing + // refreeze: the elision also moves the frozen anchors (const-amount + // shifts fold back to the immediate form) — control_step −20 B, + // flight_seam −166 B, flight_seam_flat −168 B — all size DECREASES with + // the mask soundly kept for every unproven amount. All differentials were + // re-run on the new bytes and the goldens re-pinned (see #846 PR / + // `frozen_codegen_bytes.rs`). `SYNTH_SHIFT_MASK_ELIDE=0` restores the + // pre-flip bytes (opt-out gate in `shift_mask_elide_686.rs`). + let arm_instrs = if std::env::var("SYNTH_SHIFT_MASK_ELIDE").is_ok_and(|v| v == "0") { + arm_instrs + } else { let (out, elisions) = synth_synthesis::liveness::elide_shift_masks(&arm_instrs); if std::env::var("SYNTH_FUSE_STATS").is_ok() { eprintln!( @@ -1272,8 +1281,6 @@ fn compile_wasm_to_arm( ); } out - } else { - arm_instrs }; // VCR-RA uxth/uxtb fold (#428, #242): `movw rM,#0xffff; and rD,rN,rM` → diff --git a/crates/synth-cli/Cargo.toml b/crates/synth-cli/Cargo.toml index 0e8467b4..35772be2 100644 --- a/crates/synth-cli/Cargo.toml +++ b/crates/synth-cli/Cargo.toml @@ -58,23 +58,23 @@ exports_only_275_probe = [] # Path deps carry `version` so `cargo publish` rewrites them to the # crates.io coordinate. Bumping the workspace version requires # updating these in lockstep — see docs/release-process.md. -synth-core = { path = "../synth-core", version = "0.50.0" } -synth-frontend = { path = "../synth-frontend", version = "0.50.0" } -synth-synthesis = { path = "../synth-synthesis", version = "0.50.0" } -synth-backend = { path = "../synth-backend", version = "0.50.0" } +synth-core = { path = "../synth-core", version = "0.50.1" } +synth-frontend = { path = "../synth-frontend", version = "0.50.1" } +synth-synthesis = { path = "../synth-synthesis", version = "0.50.1" } +synth-backend = { path = "../synth-backend", version = "0.50.1" } # AArch64 host-native backend (#538) — small pure-Rust crate, always on. -synth-backend-aarch64 = { path = "../synth-backend-aarch64", version = "0.50.0" } +synth-backend-aarch64 = { path = "../synth-backend-aarch64", version = "0.50.1" } # Optional external backends -synth-backend-awsm = { path = "../synth-backend-awsm", version = "0.50.0", optional = true } -synth-backend-wasker = { path = "../synth-backend-wasker", version = "0.50.0", optional = true } -synth-backend-riscv = { path = "../synth-backend-riscv", version = "0.50.0", optional = true } +synth-backend-awsm = { path = "../synth-backend-awsm", version = "0.50.1", optional = true } +synth-backend-wasker = { path = "../synth-backend-wasker", version = "0.50.1", optional = true } +synth-backend-riscv = { path = "../synth-backend-riscv", version = "0.50.1", optional = true } # Optional translation validation — pure-Rust ordeal engine by default (#553), # no C++ toolchain needed. For the Z3 differential oracle build with # `--features verify,synth-verify/z3-solver` (+ SYNTH_SOLVER_DIFF=1 at runtime). -synth-verify = { path = "../synth-verify", version = "0.50.0", optional = true, features = ["arm"] } +synth-verify = { path = "../synth-verify", version = "0.50.1", optional = true, features = ["arm"] } # Optional PulseEngine WASM optimizer # Uncomment when loom crate is available: diff --git a/crates/synth-cli/tests/fact_spec_bounds_494.rs b/crates/synth-cli/tests/fact_spec_bounds_494.rs index e6c048e7..dd6e8ecb 100644 --- a/crates/synth-cli/tests/fact_spec_bounds_494.rs +++ b/crates/synth-cli/tests/fact_spec_bounds_494.rs @@ -232,14 +232,21 @@ fn proven_slot_bound_elides_all_eight_guards_494() { assert_eq!(s.halfword_count("poll", UDF0), 0, "all guards elided"); // The measured byte win — pinned exactly so a regression (or an - // improvement) shows as a required update, like the size oracle. + // improvement) shows as a required update, like the size oracle. These are + // the SHIPPED-DEFAULT sizes: SYNTH_SHIFT_MASK_ELIDE is default-on since + // v0.50.1 (#846), so the elision also trims the poll fixture — 6 B off the + // guarded build (232->226) and 10 B off the specialized one (104->94). The + // guard-elision win itself is unchanged (still 8 guards x 16 B = 128 B; see + // the UDF census 16->0 above); the elision just shaves 4 B MORE off the + // specialized build, so the measured delta is 128 + 4 = 132. let (b_len, s_len) = (b.func("poll").len(), s.func("poll").len()); - assert_eq!(b_len, 232, "baseline poll size drifted"); - assert_eq!(s_len, 104, "specialized poll size drifted"); + assert_eq!(b_len, 226, "baseline poll size drifted"); + assert_eq!(s_len, 94, "specialized poll size drifted"); assert_eq!( b_len - s_len, - 128, - "8 guards x 16 B (#752 shape: SUB.W+CMP+BHS+UDF+CMP+BLS+UDF)" + 132, + "128 B guard elision (8 guards x 16 B, #752 SUB.W+CMP+BHS+UDF+CMP+BLS+UDF) \ + + 4 B shift-mask elision on the specialized poll (#846, default-on)" ); // Certificate trail: one ADMIT per access, naming the obligation. diff --git a/crates/synth-cli/tests/frozen_codegen_bytes.rs b/crates/synth-cli/tests/frozen_codegen_bytes.rs index 4a42c121..a145828a 100644 --- a/crates/synth-cli/tests/frozen_codegen_bytes.rs +++ b/crates/synth-cli/tests/frozen_codegen_bytes.rs @@ -85,6 +85,7 @@ fn text_sha256(wasm: &str, backend: &str, target: &str) -> (String, usize) { .env_remove("SYNTH_RV_SHIFT_FOLD") .env_remove("SYNTH_DEAD_FRAME_ELIM") .env_remove("SYNTH_UXTH_FOLD") + .env_remove("SYNTH_SHIFT_MASK_ELIDE") .args([ "compile", path.to_str().unwrap(), @@ -225,23 +226,41 @@ fn assert_frozen(cases: &[(&str, &str, usize)], backend: &str, target: &str) { /// `frozen_fixtures_stack_fwd_escape_hatch_restores_old_bytes`, pins /// untouched). Prior goldens were control_step f962794f…/314, /// flight_seam_flat 64388237…/1014. +/// +/// Goldens RE-FROZEN for the SYNTH_SHIFT_MASK_ELIDE default-on flip (#846, +/// v0.50.0): the #686 shift-mask elision that recovers gale's gpio-thin +44 B +/// regression flipped default-on, so const-amount register shifts fold back to +/// the immediate form (mask soundly kept for every unproven amount). All three +/// shift-carrying anchors SHRANK — control_step 308→288 (−20), flight_seam +/// 870→706 (−164), flight_seam_flat 1010→842 (−168); signed_div_const is +/// byte-identical (no register shifts). Execution differentials re-run GREEN on +/// the new smaller bytes BEFORE this re-pin: control_step_differential 13/13 +/// incl. the 0x00210A55 seam, flight_seam + flight_seam_flat both 0x07FDF307, +/// the #682 mod-32 oracle (i32_shift_mask_682_differential all green, incl. the +/// wrap at amount ≥ 32), and the NEW gpio_thin_846_differential (534→506 B, +/// 75/75 mmio traces+returns bit-identical across pins incl. ≥ 32). The opt-out +/// `SYNTH_SHIFT_MASK_ELIDE=0` +/// restores the prior bytes (asserted by +/// `shift_mask_elide_686_default_is_on_and_optout_rolls_back`). Prior goldens +/// were control_step b365a29e…/308, flight_seam 28642d60…/870, flight_seam_flat +/// 7d3145c4…/1010. #[test] fn frozen_fixtures_text_is_bit_identical_oracle_001() { let cases = [ ( "control_step.wasm", - "b365a29ef47ddd3e5ef8755d54cbd0d46c504af64dd24d08e9500385f674d892", - 308usize, + "8b3f1f6fe3a40994dacca91614cc19590f330eb49e8dcc8eb5b0ffc78338a1d9", + 288usize, ), ( "flight_seam.wasm", - "28642d60533c3fc154dfc7ab27e6a9f4ffe5b0a81adfe4c0fba493d490fc8d2c", - 870, + "e7152735df88a699f4600574dc5b6f5bc34efcb8cba020f78ec78f59d9d20f8a", + 706, ), ( "flight_seam_flat.wasm", - "7d3145c4ed0493cd326f867ab068930a64f3813bce60d031b19f535d0f800998", - 1010, + "5a5d675772544662fefdee6f267d07be98da1560af0626b0671e7d79b1644f37", + 842, ), ( "signed_div_const.wasm", @@ -261,8 +280,9 @@ fn frozen_fixtures_text_is_bit_identical_oracle_001() { /// /// COMPOSITION NOTE: rolling back to the pre-STACK_FWD bytes now also requires /// opting out of the LATER spill-realloc lever (`SYNTH_SPILL_REALLOC=0`), the -/// const-CSE lever (`SYNTH_CONST_CSE=0`, #242), and the flag-audit flip-wave -/// levers (`SYNTH_DEAD_FRAME_ELIM=0` + `SYNTH_UXTH_FOLD=0`, #242) — all +/// const-CSE lever (`SYNTH_CONST_CSE=0`, #242), the flag-audit flip-wave +/// levers (`SYNTH_DEAD_FRAME_ELIM=0` + `SYNTH_UXTH_FOLD=0`, #242), and the +/// #846 shift-mask-elide lever (`SYNTH_SHIFT_MASK_ELIDE=0`, v0.50.0) — all /// default on since their own flips and would otherwise rewrite the traffic /// these goldens pin. The opt-outs compose; each is separately gated. #[test] @@ -283,6 +303,7 @@ fn frozen_fixtures_stack_fwd_escape_hatch_restores_old_bytes() { for &(wasm, golden, golden_len) in &old { let elf = format!("/tmp/frozen_nofwd_{wasm}.elf"); let out = Command::new(synth()) + .env("SYNTH_SHIFT_MASK_ELIDE", "0") .env("SYNTH_NO_STACK_FWD", "1") .env("SYNTH_SPILL_REALLOC", "0") .env("SYNTH_CONST_CSE", "0") @@ -376,6 +397,7 @@ fn frozen_fixtures_spill_realloc_escape_hatch_restores_old_bytes() { for &(wasm, golden, golden_len) in &old { let elf = format!("/tmp/frozen_nospillrealloc_{wasm}.elf"); let out = Command::new(synth()) + .env("SYNTH_SHIFT_MASK_ELIDE", "0") .env("SYNTH_SPILL_REALLOC", "0") .env("SYNTH_CONST_CSE", "0") .env("SYNTH_DEAD_FRAME_ELIM", "0") @@ -464,6 +486,7 @@ fn frozen_fixtures_const_cse_escape_hatch_restores_old_bytes() { for &(wasm, golden, golden_len) in &old { let elf = format!("/tmp/frozen_nocse_{wasm}.elf"); let out = Command::new(synth()) + .env("SYNTH_SHIFT_MASK_ELIDE", "0") .env("SYNTH_CONST_CSE", "0") .env("SYNTH_DEAD_FRAME_ELIM", "0") .env("SYNTH_UXTH_FOLD", "0") @@ -552,6 +575,7 @@ fn frozen_fixtures_dead_frame_elim_escape_hatch_restores_old_bytes() { for &(wasm, golden, golden_len) in &old { let elf = format!("/tmp/frozen_nodfe_{wasm}.elf"); let out = Command::new(synth()) + .env("SYNTH_SHIFT_MASK_ELIDE", "0") .env("SYNTH_DEAD_FRAME_ELIM", "0") .env_remove("SYNTH_UXTH_FOLD") .env_remove("SYNTH_NO_CMP_SELECT_FUSE") @@ -629,6 +653,7 @@ fn frozen_fixtures_uxth_fold_escape_hatch_restores_old_bytes() { for &(wasm, golden, golden_len) in &old { let elf = format!("/tmp/frozen_nouxth_{wasm}.elf"); let out = Command::new(synth()) + .env("SYNTH_SHIFT_MASK_ELIDE", "0") .env("SYNTH_UXTH_FOLD", "0") .env_remove("SYNTH_DEAD_FRAME_ELIM") .env_remove("SYNTH_NO_CMP_SELECT_FUSE") diff --git a/crates/synth-cli/tests/parity_benchmark_735.rs b/crates/synth-cli/tests/parity_benchmark_735.rs index 7cb8665b..04a2946f 100644 --- a/crates/synth-cli/tests/parity_benchmark_735.rs +++ b/crates/synth-cli/tests/parity_benchmark_735.rs @@ -16,7 +16,8 @@ //! * gust_mix clamp, SYNTH_FACT_SPEC + premise — 14 B (verify feature only: //! the fact-spec pass needs the ordeal solver; cfg-gated below, exercised //! by the fact-spec CI job via `--features verify`) -//! * flat_flight (loom-dissolved), default — 458 B +//! * flat_flight (loom-dissolved), default — 384 B (was 458; #846 +//! shift-mask default-on, execution-verified 24/24) //! * falcon_axis f32 (cortex-m4f, VFP) — 72 B //! //! The native-C comparison numbers are NOT pinned here — they belong to @@ -160,7 +161,11 @@ fn clamp_default_path_is_pinned_735() { fn flat_flight_is_pinned_735() { let wasm = std::fs::read(repro("flat_flight/flat_flight.loom.wasm")).expect("read fixture"); let sizes = per_function_sizes(&wasm, "flat_flight", "cortex-m4", false); - assert_pinned(&sizes, "flat_flight", 458); + // 458 -> 384 (-74 B) with the #846 SYNTH_SHIFT_MASK_ELIDE default-on flip: + // flat_flight's const-amount register shifts drop the redundant #682 mod-32 + // re-mask. Execution UNCHANGED — re-pinned only after 24/24 seeds proved + // flag-ON ≡ flag-OFF ≡ wasmtime (return + full linear-memory image). + assert_pinned(&sizes, "flat_flight", 384); } /// falcon-style f32 complementary-filter axis (VFP, cortex-m4f): the diff --git a/crates/synth-cli/tests/shift_mask_elide_686.rs b/crates/synth-cli/tests/shift_mask_elide_686.rs index efe830cf..40a7f597 100644 --- a/crates/synth-cli/tests/shift_mask_elide_686.rs +++ b/crates/synth-cli/tests/shift_mask_elide_686.rs @@ -3,11 +3,14 @@ //! //! Three gates, per the flag-then-flip protocol: //! -//! 1. **Default is OFF and byte-identical to baseline** — the elision moves -//! the frozen anchors (const-amount shifts fold back to the immediate -//! form), so it lands opt-in. `frozen_codegen_bytes.rs` locks the shipped -//! bytes; this test additionally pins unset ≡ explicit opt-out, so the -//! default can only change via a deliberate flip (which updates this). +//! 1. **Default is ON since v0.50.0 (#846)** — the flag flipped default-on to +//! recover gale's gpio-thin +44 B regression (the pin bit-arithmetic emits +//! `and rN,#0x1f` then the redundant #682 re-mask `and r12,#0x1f`). The +//! flip re-froze the anchors (`frozen_codegen_bytes.rs`, all differentials +//! re-run green on the new smaller bytes). This gate now pins unset ≡ +//! explicit `SYNTH_SHIFT_MASK_ELIDE=1` (the ON default) AND that the +//! opt-out `SYNTH_SHIFT_MASK_ELIDE=0` STILL rolls back to the pre-flip +//! bytes byte-for-byte — the escape hatch every flip lever owes. //! 2. **Per-function no-grow table** — with the flag ON, no function in the //! corpus gets BIGGER on either path (relocatable/direct and default/ //! optimized). Elision is removal/rewrite-only; growth would mean the @@ -47,6 +50,7 @@ const CORPUS: &[&str] = &[ "signed_div_const.wasm", "i32_shift_mask_682.wat", "gust_mix_686.wat", + "gpio_thin_846.loom.wasm", ]; /// Both codegen paths: `--relocatable` forces the direct stack selector; the @@ -120,21 +124,31 @@ fn compile(wasm: &str, relocatable: bool, elide: Option<&str>) -> (Vec, BTre (data, sizes) } -/// Gate 1: unset ≡ `SYNTH_SHIFT_MASK_ELIDE=0`, byte-for-byte — the flag is -/// opt-in. A deliberate default-on flip re-freezes the anchors and updates -/// this assertion (flag-then-flip protocol; the maintainer owns the flip). +/// Gate 1 (post-#846 flip): unset ≡ `SYNTH_SHIFT_MASK_ELIDE=1`, byte-for-byte +/// — the flag is DEFAULT-ON since v0.50.0. Also proves the opt-out escape +/// hatch: `SYNTH_SHIFT_MASK_ELIDE=0` must differ from the default on at least +/// one shift-heavy fixture (else the rollback lever is vacuous). #[test] -fn shift_mask_elide_686_default_is_off_and_byte_identical() { +fn shift_mask_elide_686_default_is_on_and_optout_rolls_back() { + let mut optout_differs = false; for &(vname, reloc) in VARIANTS { for &wasm in CORPUS { let (unset, _) = compile(wasm, reloc, None); - let (off, _) = compile(wasm, reloc, Some("0")); + let (on, _) = compile(wasm, reloc, Some("1")); assert_eq!( - unset, off, - "{wasm} [{vname}]: default must equal explicit opt-out (flag is opt-in)" + unset, on, + "{wasm} [{vname}]: default must equal explicit ON (flag is default-on since #846)" ); + let (off, _) = compile(wasm, reloc, Some("0")); + if off != unset { + optout_differs = true; + } } } + assert!( + optout_differs, + "SYNTH_SHIFT_MASK_ELIDE=0 never changed bytes — the opt-out rollback is vacuous" + ); } /// Gates 2+3: per-function no-grow across the corpus, strict shrink on the diff --git a/crates/synth-cli/tests/size_attribution_390.rs b/crates/synth-cli/tests/size_attribution_390.rs index 7e13797e..9958b9e8 100644 --- a/crates/synth-cli/tests/size_attribution_390.rs +++ b/crates/synth-cli/tests/size_attribution_390.rs @@ -122,11 +122,17 @@ fn per_function_sizes() -> BTreeMap { /// origin/main binary), incl. gust_spill_fwd_390_differential.py (gust_poll /// return + post-call state struct vs wasmtime in default + both lever /// opt-outs). Only gust_poll's bytes changed; func_0/func_1/gust_mix identical. +// RE-PINNED for the #846 SYNTH_SHIFT_MASK_ELIDE default-on flip (v0.50.0): the +// scheduler's pin/shift bit-arithmetic drops the redundant #682 mod-32 re-mask, +// shrinking gust_poll 716→692 (−24), func_0 408→380 (−28), func_1 76→60 (−16); +// gust_mix (no register shifts) is unchanged. Execution UNCHANGED — re-pinned +// only after gust_spill_fwd_390_differential.py PASSED on the new default-on +// bytes (gust_poll return + post-call state struct vs wasmtime). const LOCKED: &[(&str, u64)] = &[ - ("gust_poll", 716), + ("gust_poll", 692), ("gust_mix", 32), - ("func_0", 408), - ("func_1", 76), + ("func_0", 380), + ("func_1", 60), ]; #[test] diff --git a/crates/synth-frontend/Cargo.toml b/crates/synth-frontend/Cargo.toml index a50c33df..5c976bc4 100644 --- a/crates/synth-frontend/Cargo.toml +++ b/crates/synth-frontend/Cargo.toml @@ -14,7 +14,7 @@ description = "WASM/WAT parser and module decoder frontend for the Synth compile # Internal path deps carry an explicit version so `cargo publish` # can rewrite to the crates.io coordinate. `path` is used for # in-workspace builds; `version` is what crates.io sees. -synth-core = { path = "../synth-core", version = "0.50.0" } +synth-core = { path = "../synth-core", version = "0.50.1" } wasmparser.workspace = true wasm-encoder.workspace = true diff --git a/crates/synth-opt/Cargo.toml b/crates/synth-opt/Cargo.toml index 1a7ebc2a..bc3687a6 100644 --- a/crates/synth-opt/Cargo.toml +++ b/crates/synth-opt/Cargo.toml @@ -11,7 +11,7 @@ categories.workspace = true description = "Peephole optimization passes for the Synth compiler" [dependencies] -synth-cfg = { path = "../synth-cfg", version = "0.50.0" } +synth-cfg = { path = "../synth-cfg", version = "0.50.1" } [dev-dependencies] criterion = { version = "0.8", features = ["html_reports"] } diff --git a/crates/synth-synthesis/Cargo.toml b/crates/synth-synthesis/Cargo.toml index 3451f2f5..8b5611ce 100644 --- a/crates/synth-synthesis/Cargo.toml +++ b/crates/synth-synthesis/Cargo.toml @@ -11,9 +11,9 @@ categories.workspace = true description = "WASM-to-ARM instruction selection and peephole optimizer" [dependencies] -synth-core = { path = "../synth-core", version = "0.50.0" } -synth-cfg = { path = "../synth-cfg", version = "0.50.0" } -synth-opt = { path = "../synth-opt", version = "0.50.0" } +synth-core = { path = "../synth-core", version = "0.50.1" } +synth-cfg = { path = "../synth-cfg", version = "0.50.1" } +synth-opt = { path = "../synth-opt", version = "0.50.1" } serde.workspace = true anyhow.workspace = true thiserror.workspace = true diff --git a/crates/synth-verify/Cargo.toml b/crates/synth-verify/Cargo.toml index 1db90f6a..55d2cfae 100644 --- a/crates/synth-verify/Cargo.toml +++ b/crates/synth-verify/Cargo.toml @@ -22,12 +22,12 @@ arm = ["synth-synthesis"] [dependencies] # Core dependencies (always required) -synth-core = { path = "../synth-core", version = "0.50.0" } -synth-cfg = { path = "../synth-cfg", version = "0.50.0" } -synth-opt = { path = "../synth-opt", version = "0.50.0" } +synth-core = { path = "../synth-core", version = "0.50.1" } +synth-cfg = { path = "../synth-cfg", version = "0.50.1" } +synth-opt = { path = "../synth-opt", version = "0.50.1" } # ARM synthesis (optional, behind 'arm' feature) -synth-synthesis = { path = "../synth-synthesis", version = "0.50.0", optional = true } +synth-synthesis = { path = "../synth-synthesis", version = "0.50.1", optional = true } # Default SMT engine: pure-Rust, certificate-checked QF_BV solver (#553). # 0.9 adds the `ordeal::trap` module (trap-preservation VCs, VCR-VER-002 / #166); diff --git a/docs/status/FEATURE_MATRIX.md b/docs/status/FEATURE_MATRIX.md index e437a66e..9adce476 100644 --- a/docs/status/FEATURE_MATRIX.md +++ b/docs/status/FEATURE_MATRIX.md @@ -7,7 +7,7 @@ > stale. All numbers come from [`artifacts/status.json`](../../artifacts/status.json), > which is re-derived from source on every run — never hand-edited. -**Workspace version:** 0.50.0 +**Workspace version:** 0.50.1 --- diff --git a/npm/package.json b/npm/package.json index 371eda0b..86feca35 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "@pulseengine/synth", - "version": "0.50.0", + "version": "0.50.1", "description": "synth — a WebAssembly-to-ARM/RISC-V/AArch64 compiler with mechanized correctness proofs. Produces bare-metal ELF binaries for embedded targets.", "bin": { "synth": "./run.js" diff --git a/scripts/repro/fact_spec_bounds_494_differential.py b/scripts/repro/fact_spec_bounds_494_differential.py index 8699943f..4502e4f8 100644 --- a/scripts/repro/fact_spec_bounds_494_differential.py +++ b/scripts/repro/fact_spec_bounds_494_differential.py @@ -318,12 +318,17 @@ def main(): s_len = func_size(spec_elf, "poll") f_len = func_size(floor_elf, "poll") print(f"poll: guarded {b_len} B -> specialized {s_len} B " - f"(-{b_len - s_len} B = 8 guards x 16 B SUB/CMP/BHS/UDF/CMP/BLS/UDF, " - f"the #752 wraparound-safe shape; the #390 " - f"guard_bool instruction class — gust_poll's whole guard_bool " - f"bucket is 108 B); unguarded floor {f_len} B") - if b_len - s_len != 128: - sys.exit(f"byte win drifted: expected exactly 128 B " + f"(-{b_len - s_len} B = 128 B guard elision [8 guards x 16 B " + f"SUB/CMP/BHS/UDF/CMP/BLS/UDF, the #752 wraparound-safe shape; the " + f"#390 guard_bool class, gust_poll's whole guard_bool bucket is 108 B] " + f"+ 4 B from SYNTH_SHIFT_MASK_ELIDE, default-on since v0.50.1 (#846): " + f"the elision trims BOTH builds but 4 B more off the specialized poll " + f"[guarded 232->226, specialized 104->94]); unguarded floor {f_len} B") + # 132 = 128 (guard elision) + 4 (shift-mask elision fires more on the + # specialized build; SYNTH_SHIFT_MASK_ELIDE default-on since v0.50.1/#846). + # With SYNTH_SHIFT_MASK_ELIDE=0 this is exactly 128 (the pre-flip win). + if b_len - s_len != 132: + sys.exit(f"byte win drifted: expected exactly 132 B " f"(got {b_len - s_len})") scode, sbase, ssyms = load(spec_elf) fcode, _, _ = load(floor_elf) diff --git a/scripts/repro/gpio_thin_846.loom.wasm b/scripts/repro/gpio_thin_846.loom.wasm new file mode 100644 index 00000000..aadb6a5f Binary files /dev/null and b/scripts/repro/gpio_thin_846.loom.wasm differ diff --git a/scripts/repro/gpio_thin_846_differential.py b/scripts/repro/gpio_thin_846_differential.py new file mode 100644 index 00000000..fe5bdd86 --- /dev/null +++ b/scripts/repro/gpio_thin_846_differential.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +"""#846 gpio-thin size-regression gate — SIZE drop + execution-UNCHANGED. + +gale's `gpio-thin` gust driver regressed +44 B / +9% (490→534 `.text`) on synth +0.49: its pin bit-arithmetic (`pin & 0xf` / `pin & 31` then a register shift) +emits the source mask `and rN,#0x1f` IMMEDIATELY followed by the #682 mod-32 +re-mask `and r12,rN,#0x1f`, and that second mask is provably redundant (the +operand is already in-range). The #686 `elide_shift_masks` Pattern B drops it; +v0.50.0 flips that pass DEFAULT-ON (`SYNTH_SHIFT_MASK_ELIDE=0` opts out). + +This gate asserts BOTH halves of the fix on gale's REAL loom.wasm (pinned as +`gpio_thin_846.loom.wasm`, decoded from the issue), never a synthetic (#757): + + (a) SIZE: default (flag-on) `.text` < opt-out (flag-off) `.text`, and the + redundant `and r12,#0x1f` count strictly drops. (Both compiled here.) + (b) EXECUTION UNCHANGED: for a pin sweep that INCLUDES pin >= 32 (the mod-32 + boundary a wrongly-elided mask would corrupt), the captured + mmio_write32 (addr,value) call sequence AND every mmio_read32-consuming + return value are BIT-IDENTICAL between: + - wasmtime (ground truth, imports trampolined to a fake MMIO), and + - unicorn running synth's flag-ON ARM (BL to the unresolved + mmio_write32/read32 imports hooked to the same fake MMIO). + A size fix that changed behavior would be a miscompile; this is the + whole point of the issue (effects identical, verified by gale's Renode + content-gate). + +Deps: wasmtime unicorn pyelftools capstone. +Run: + SYNTH=./target/debug/synth python3 scripts/repro/gpio_thin_846_differential.py +Exits nonzero on any size or execution mismatch. +""" +import os +import struct +import subprocess +import sys +import tempfile + +import wasmtime +from capstone import CS_ARCH_ARM, CS_MODE_THUMB, Cs +from elftools.elf.elffile import ELFFile +from unicorn import UC_ARCH_ARM, UC_HOOK_CODE, UC_MODE_THUMB, Uc, UcError +from unicorn.arm_const import ( + UC_ARM_REG_LR, + UC_ARM_REG_PC, + UC_ARM_REG_R0, + UC_ARM_REG_R1, + UC_ARM_REG_R2, + UC_ARM_REG_R11, + UC_ARM_REG_SP, +) + +SYNTH = os.environ.get("SYNTH", "./target/release/synth") +HERE = os.path.dirname(os.path.abspath(__file__)) +WASM = os.path.join(HERE, "gpio_thin_846.loom.wasm") + +# Fake MMIO peripheral: a flat dict addr -> u32. mmio_write32 stores, +# mmio_read32 returns whatever was last written (default 0). A deterministic +# non-zero seed exercises the read-modify-write drivers (clear/toggle/config). +BASE_ADDR = 0x40020000 +SEED = {BASE_ADDR + 0x10: 0xA5A50F0F} # ODR-ish register some drivers read + +# Pin sweep: crucially includes >= 32 (mod-32 boundary), the wrap the mask +# guards. Also a spread of the `& 0xf` (config) domain and typical GPIO pins. +PINS = [0, 1, 5, 7, 8, 15, 16, 31, 32, 33, 47, 63, 100, 255, 0x7FFFFFFF] + + +def compile_elf(flag_off, out): + env = dict(os.environ) + if flag_off: + env["SYNTH_SHIFT_MASK_ELIDE"] = "0" + else: + env.pop("SYNTH_SHIFT_MASK_ELIDE", None) # default = ON + subprocess.run( + [SYNTH, "compile", WASM, "-o", out, + "--target", "cortex-m3", "--all-exports", "--relocatable"], + env=env, check=True, capture_output=True, + ) + + +def text_size_and_masks(elf): + """(.text length, count of `and r12,rN,#0x1f` mod-32 masks) from the ELF.""" + with open(elf, "rb") as fh: + e = ELFFile(fh) + text = e.get_section_by_name(".text") + code, base = text.data(), text["sh_addr"] + md = Cs(CS_ARCH_ARM, CS_MODE_THUMB) + masks = 0 + for ins in md.disasm(bytes(code), base): + # capstone renders R12 as its alias `ip`; match either spelling. + rd = ins.op_str.split(",", 1)[0].strip() + if (ins.mnemonic.startswith("and") and rd in ("r12", "ip") + and "#0x1f" in ins.op_str): + masks += 1 + return len(code), masks + + +def elf_syms_text(path): + with open(path, "rb") as fh: + elf = ELFFile(fh) + symtab = next( + (s for s in elf.iter_sections() if s["sh_type"] == "SHT_SYMTAB"), None) + if symtab is None: + sys.exit(f"no SHT_SYMTAB in {path}") + syms = {s.name: s["st_value"] & ~1 + for s in symtab.iter_symbols() + if s.name and s["st_info"]["type"] == "STT_FUNC"} + text = elf.get_section_by_name(".text") + return syms, text.data(), text["sh_addr"] + + +# --- wasmtime ground truth: run one export, capture the mmio trace + return --- +def wasmtime_trace(export, args): + engine = wasmtime.Engine() + module = wasmtime.Module(engine, open(WASM, "rb").read()) + store = wasmtime.Store(engine) + trace = [] + state = dict(SEED) + + def mmio_write32(addr, val): + trace.append(("w", addr & 0xFFFFFFFF, val & 0xFFFFFFFF)) + state[addr & 0xFFFFFFFF] = val & 0xFFFFFFFF + + def mmio_read32(addr): + v = state.get(addr & 0xFFFFFFFF, 0) + trace.append(("r", addr & 0xFFFFFFFF, v)) + return v + + ft_w = wasmtime.FuncType([wasmtime.ValType.i32(), wasmtime.ValType.i32()], []) + ft_r = wasmtime.FuncType([wasmtime.ValType.i32()], [wasmtime.ValType.i32()]) + imports = [ + wasmtime.Func(store, ft_w, mmio_write32), + wasmtime.Func(store, ft_r, mmio_read32), + ] + inst = wasmtime.Instance(store, module, imports) + fn = inst.exports(store)[export] + ret = fn(store, *args) + ret = (ret & 0xFFFFFFFF) if isinstance(ret, int) else None + return trace, ret + + +# --- unicorn: run synth's ARM for one export, same fake MMIO via BL hooks --- +CODE, LIN, STK = 0x100000, 0x40000, 0x90000 +RET = 0x9F00 | 1 + + +def unicorn_trace(syms, code, base, export, idx, args, wpc, rpc): + trace = [] + state = dict(SEED) + mu = Uc(UC_ARCH_ARM, UC_MODE_THUMB) + mu.mem_map(CODE, 0x20000) + mu.mem_map(LIN, 0x20000) + mu.mem_map(STK - 0x8000, 0x10000) + mu.mem_write(CODE, code) + + entry = None + for cand in (export, f"func_{idx}"): + if cand in syms: + entry = syms[cand] + break + if entry is None: + sys.exit(f"SYMBOL MISSING: {export}; symtab has {sorted(syms)}") + + # Args in r0.., linmem base in r11. + regs = [UC_ARM_REG_R0, UC_ARM_REG_R1, UC_ARM_REG_R2] + for r, v in zip(regs, args): + mu.reg_write(r, v & 0xFFFFFFFF) + mu.reg_write(UC_ARM_REG_SP, STK) + mu.reg_write(UC_ARM_REG_R11, LIN) + mu.reg_write(UC_ARM_REG_LR, RET) + + # BL to an import lands on the (unresolved, addend -4) placeholder; its + # target after relocation would be mmio_write32/read32. We identify the + # call by the reloc'd symbol via the ELF: the BL site PC is whatever the + # placeholder points at. Simpler + robust: hook every `bl`/`blx` and + # decide by which import symbol the reloc names at that offset. + def do_write(mu): + addr = mu.reg_read(UC_ARM_REG_R0) & 0xFFFFFFFF + val = mu.reg_read(UC_ARM_REG_R1) & 0xFFFFFFFF + trace.append(("w", addr, val)) + state[addr] = val + + def do_read(mu): + addr = mu.reg_read(UC_ARM_REG_R0) & 0xFFFFFFFF + v = state.get(addr, 0) + trace.append(("r", addr, v)) + mu.reg_write(UC_ARM_REG_R0, v) + + def hook(mu, a, sz, u): + off = a - CODE + if off in wpc: + do_write(mu) + mu.reg_write(UC_ARM_REG_PC, ((a + sz) | 1)) # return past the BL + elif off in rpc: + do_read(mu) + mu.reg_write(UC_ARM_REG_PC, ((a + sz) | 1)) + mu.hook_add(UC_HOOK_CODE, hook) + + try: + mu.emu_start((CODE + entry - base) | 1, RET & ~1, count=4000) + ret = mu.reg_read(UC_ARM_REG_R0) & 0xFFFFFFFF + except UcError as e: + return trace, f"ERR:{e}" + return trace, ret + + +def import_call_sites(elf_path, base): + """Map each `bl`-to-import call-site .text offset -> 'w'|'r' via relocs. + + synth's relocatable ELF carries R_ARM_THM_CALL relocations naming the + import symbol (mmio_write32 / mmio_read32) at each BL site. We return the + sets of .text offsets that call write vs read. + """ + wpc, rpc = set(), set() + with open(elf_path, "rb") as fh: + elf = ELFFile(fh) + symtab = next((s for s in elf.iter_sections() + if s["sh_type"] == "SHT_SYMTAB"), None) + symnames = [s.name for s in symtab.iter_symbols()] + for sec in elf.iter_sections(): + if sec["sh_type"] not in ("SHT_REL", "SHT_RELA"): + continue + # Only relocations against .text. + for reloc in sec.iter_relocations(): + symidx = reloc["r_info_sym"] + name = symnames[symidx] if symidx < len(symnames) else "" + off = reloc["r_offset"] + if name == "mmio_write32": + wpc.add(off) + elif name == "mmio_read32": + rpc.add(off) + return wpc, rpc + + +EXPORTS = [ + ("gpio_clear", 2, lambda pin: (BASE_ADDR, pin)), + ("gpio_read", 4, lambda pin: (BASE_ADDR, pin)), + ("gpio_set", 5, lambda pin: (BASE_ADDR, pin)), + ("gpio_toggle", 6, lambda pin: (BASE_ADDR, pin)), + ("gpio_configure", 3, lambda pin: (BASE_ADDR, pin, 0x3)), +] + + +def main(): + tmp = tempfile.mkdtemp() + elf_on = os.path.join(tmp, "gpio_on.elf") + elf_off = os.path.join(tmp, "gpio_off.elf") + compile_elf(flag_off=False, out=elf_on) + compile_elf(flag_off=True, out=elf_off) + + # (a) SIZE half. + on_len, on_masks = text_size_and_masks(elf_on) + off_len, off_masks = text_size_and_masks(elf_off) + print("=== #846 SIZE ===") + print(f" flag-OFF (SYNTH_SHIFT_MASK_ELIDE=0): .text={off_len} B " + f"redundant `and r12,#0x1f` masks={off_masks}") + print(f" flag-ON (default v0.50.0) : .text={on_len} B " + f"redundant `and r12,#0x1f` masks={on_masks}") + print(f" recovered: {off_len - on_len} B ({off_masks - on_masks} masks elided)") + size_ok = on_len < off_len and on_masks < off_masks + if not size_ok: + print(" SIZE: FAIL (default did not shrink .text / mask count)") + + # (b) EXECUTION half — on the flag-ON bytes, across the pin sweep incl >=32. + syms, code, base = elf_syms_text(elf_on) + wpc, rpc = import_call_sites(elf_on, base) + print("=== #846 EXECUTION UNCHANGED (flag-ON bytes vs wasmtime) ===") + exec_ok = True + checks = 0 + for export, idx, mkargs in EXPORTS: + for pin in PINS: + args = mkargs(pin) + gt_trace, gt_ret = wasmtime_trace(export, args) + un_trace, un_ret = unicorn_trace( + syms, code, base, export, idx, args, wpc, rpc) + ok = (gt_trace == un_trace) and (gt_ret == un_ret + if gt_ret is not None else True) + checks += 1 + if not ok: + exec_ok = False + print(f" MISMATCH {export}(pin={pin}):") + print(f" wasmtime trace={gt_trace} ret={gt_ret}") + print(f" unicorn trace={un_trace} ret={un_ret}") + if exec_ok: + print(f" {checks}/{checks} match — mmio (addr,value) sequences + returns " + f"bit-identical across pins {PINS} (incl. >=32 mod-32 boundary)") + + print("=== RESULT ===") + ok = size_ok and exec_ok + print("gpio-thin #846: PASS" if ok else "gpio-thin #846: FAIL") + sys.exit(0 if ok else 1) + + +if __name__ == "__main__": + main() diff --git a/scripts/repro/vcr_dec_001_graph_alloc_differential.py b/scripts/repro/vcr_dec_001_graph_alloc_differential.py index 874cd556..e227b01b 100644 --- a/scripts/repro/vcr_dec_001_graph_alloc_differential.py +++ b/scripts/repro/vcr_dec_001_graph_alloc_differential.py @@ -50,12 +50,16 @@ # Canonical frozen fixtures (fixture, golden .text sha256, golden len) — the exact # pins from crates/synth-cli/tests/frozen_codegen_bytes.rs oracle_001. FROZEN = [ + # Synced to frozen_codegen_bytes.rs oracle_001 default block after the #846 + # shift-mask elision went default-on (v0.50.1): the shift-carrying anchors + # SHRANK — control_step 308→288 (−20), flight_seam 870→706 (−164), + # flight_seam_flat 1010→842 (−168). signed_div_const is inert (no shift). ("control_step.wasm", - "b365a29ef47ddd3e5ef8755d54cbd0d46c504af64dd24d08e9500385f674d892", 308), + "8b3f1f6fe3a40994dacca91614cc19590f330eb49e8dcc8eb5b0ffc78338a1d9", 288), ("flight_seam.wasm", - "28642d60533c3fc154dfc7ab27e6a9f4ffe5b0a81adfe4c0fba493d490fc8d2c", 870), + "e7152735df88a699f4600574dc5b6f5bc34efcb8cba020f78ec78f59d9d20f8a", 706), ("flight_seam_flat.wasm", - "7d3145c4ed0493cd326f867ab068930a64f3813bce60d031b19f535d0f800998", 1010), + "5a5d675772544662fefdee6f267d07be98da1560af0626b0671e7d79b1644f37", 842), ("signed_div_const.wasm", "b277453b7829a5b2c64527131298d89fa63f5641231b1d4c7336675e8cdab9b0", 34), ] @@ -73,6 +77,9 @@ "SYNTH_NO_CMP_SELECT_FUSE", "SYNTH_NO_LOCAL_PROMOTE", "SYNTH_NO_IMM_SHIFT_FOLD", "SYNTH_NO_STACK_FWD", "SYNTH_SPILL_REALLOC", "SYNTH_CONST_CSE", "SYNTH_BASE_CSE", "SYNTH_DEAD_FRAME_ELIM", "SYNTH_UXTH_FOLD", "SYNTH_GRAPH_ALLOC", + # #846: shift-mask elision is default-on since v0.50.1; strip its opt-out + # (`SYNTH_SHIFT_MASK_ELIDE=0`) so an ambient override can't un-freeze the gate. + "SYNTH_SHIFT_MASK_ELIDE", ]