From 85146d3917f109ac3a18b8662e2495dc87d09a8e Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Wed, 24 Jun 2026 18:36:45 +0200 Subject: [PATCH] =?UTF-8?q?feat(vcr-ra):=20ship=20immediate-shift=20foldin?= =?UTF-8?q?g=20default-on=20=E2=80=94=20v0.15.0=20(#390,=20#242)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flips the immediate-shift folding peephole default-on (PR #463 landed it flag-off). A constant shift amount the stack selector materialized into a scratch register (`movw rM,#C; lsl rD,rN,rM`) now folds to the immediate form (`lsl rD,rN,#C`), removing the dead `movw` — −1 instruction, −1 live register per folded shift. Flip: arm_backend.rs default-ON with opt-out SYNTH_NO_IMM_SHIFT_FOLD=1. Re-froze the ARM goldens (control_step 316→304, flight_seam 866→774, flight_seam_flat 1006→910 = −200 B; signed_div_const unchanged — no register-shift folds). RV32 gate untouched (ARM-only peephole). Results preserved across the byte change: control_step 0x00210A55 (differential 13/13), flat+inlined flight_algo 0x07FDF307 (MATCH); opt-out restores the v0.14.0 bytes; full workspace suite green. Validated bit-identical + a net cycle win on the dissolved hot path (−2 cyc/call, .text 100→90 B on gust_mix). Cumulative dissolved hot-path: 64.0 → 58.0 (cmp→select) → 50.0 (local promotion) → 48.0 cyc/call. Pin-swept 0.14.0→0.15.0; CHANGELOG added. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 23 +++++++++++++ Cargo.lock | 34 +++++++++---------- Cargo.toml | 2 +- MODULE.bazel | 2 +- crates/synth-backend-awsm/Cargo.toml | 2 +- crates/synth-backend-riscv/Cargo.toml | 4 +-- crates/synth-backend-wasker/Cargo.toml | 2 +- crates/synth-backend/Cargo.toml | 4 +-- crates/synth-backend/src/arm_backend.rs | 9 +++-- crates/synth-cli/Cargo.toml | 16 ++++----- .../synth-cli/tests/frozen_codegen_bytes.rs | 33 +++++++++--------- crates/synth-frontend/Cargo.toml | 2 +- crates/synth-opt/Cargo.toml | 2 +- crates/synth-synthesis/Cargo.toml | 6 ++-- crates/synth-verify/Cargo.toml | 8 ++--- 15 files changed, 86 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bce6352..7157cd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.15.0] - 2026-06-24 + +**Immediate-shift folding is now DEFAULT-ON (VCR-RA, #390, epic #242).** The stack +selector lowers `i32.shl/shr_s/shr_u` to register-shift forms, materializing a +constant shift amount into a scratch register (`movw rM,#C; lsl rD,rN,rM`); the +peephole now folds that to the immediate form (`lsl rD,rN,#C`) and removes the dead +`movw` — −1 instruction and −1 live register per folded shift. This is a +**byte-changing** release on top of v0.14.0: `.text` shrinks on shift-heavy +functions — control_step 316→304 B, flight_seam 866→774, flight_seam_flat 1006→910 +(−200 B across the frozen fixtures); signed_div_const (no register-shift folds) +unchanged. + +Execution results are unchanged: control_step `0x00210A55` (differential 13/13) and +flat+inlined flight_algo `0x07FDF307` are preserved. Validated bit-identical and a +net cycle win on the dissolved hot path (−2 cyc/call, `.text` 100→90 B on gust_mix). +Cumulative dissolved hot-path improvement across the codegen levers: +64.0 → 58.0 (cmp→select) → 50.0 (local promotion) → 48.0 cyc/call. + +Soundness: only folds shift amounts in [1,31] (where register- and immediate-shift +forms provably agree), the shift-amount register must be dead after the shift, and +the rewrite is removal-only (offset-neutral before branch resolution). Escape hatch: +`SYNTH_NO_IMM_SHIFT_FOLD=1` restores the register-shift form. + ## [0.14.0] - 2026-06-24 **i32 local promotion is now DEFAULT-ON (VCR-RA-001, #390, epic #242).** The ARM diff --git a/Cargo.lock b/Cargo.lock index fc42170..ec86c7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2023,14 +2023,14 @@ dependencies = [ [[package]] name = "synth-abi" -version = "0.14.0" +version = "0.15.0" dependencies = [ "synth-wit", ] [[package]] name = "synth-analysis" -version = "0.14.0" +version = "0.15.0" dependencies = [ "anyhow", "synth-core", @@ -2039,7 +2039,7 @@ dependencies = [ [[package]] name = "synth-backend" -version = "0.14.0" +version = "0.15.0" dependencies = [ "anyhow", "synth-core", @@ -2049,7 +2049,7 @@ dependencies = [ [[package]] name = "synth-backend-awsm" -version = "0.14.0" +version = "0.15.0" dependencies = [ "anyhow", "synth-core", @@ -2058,7 +2058,7 @@ dependencies = [ [[package]] name = "synth-backend-riscv" -version = "0.14.0" +version = "0.15.0" dependencies = [ "anyhow", "proptest", @@ -2070,7 +2070,7 @@ dependencies = [ [[package]] name = "synth-backend-wasker" -version = "0.14.0" +version = "0.15.0" dependencies = [ "anyhow", "synth-core", @@ -2079,11 +2079,11 @@ dependencies = [ [[package]] name = "synth-cfg" -version = "0.14.0" +version = "0.15.0" [[package]] name = "synth-cli" -version = "0.14.0" +version = "0.15.0" dependencies = [ "anyhow", "clap", @@ -2108,7 +2108,7 @@ dependencies = [ [[package]] name = "synth-core" -version = "0.14.0" +version = "0.15.0" dependencies = [ "anyhow", "gimli", @@ -2122,7 +2122,7 @@ dependencies = [ [[package]] name = "synth-frontend" -version = "0.14.0" +version = "0.15.0" dependencies = [ "anyhow", "synth-core", @@ -2136,14 +2136,14 @@ dependencies = [ [[package]] name = "synth-memory" -version = "0.14.0" +version = "0.15.0" dependencies = [ "bitflags", ] [[package]] name = "synth-opt" -version = "0.14.0" +version = "0.15.0" dependencies = [ "criterion", "synth-cfg", @@ -2151,11 +2151,11 @@ dependencies = [ [[package]] name = "synth-qemu" -version = "0.14.0" +version = "0.15.0" [[package]] name = "synth-synthesis" -version = "0.14.0" +version = "0.15.0" dependencies = [ "anyhow", "proptest", @@ -2170,7 +2170,7 @@ dependencies = [ [[package]] name = "synth-test" -version = "0.14.0" +version = "0.15.0" dependencies = [ "anyhow", "clap", @@ -2186,7 +2186,7 @@ dependencies = [ [[package]] name = "synth-verify" -version = "0.14.0" +version = "0.15.0" dependencies = [ "anyhow", "chrono", @@ -2204,7 +2204,7 @@ dependencies = [ [[package]] name = "synth-wit" -version = "0.14.0" +version = "0.15.0" [[package]] name = "tempfile" diff --git a/Cargo.toml b/Cargo.toml index 767f559..ea78102 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,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.14.0" +version = "0.15.0" edition = "2024" rust-version = "1.88" authors = ["PulseEngine Team"] diff --git a/MODULE.bazel b/MODULE.bazel index 1f7c270..32e2907 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.14.0", + version = "0.15.0", ) # Bazel dependencies diff --git a/crates/synth-backend-awsm/Cargo.toml b/crates/synth-backend-awsm/Cargo.toml index 1bc3e21..eee3266 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.14.0" } +synth-core = { path = "../synth-core", version = "0.15.0" } anyhow.workspace = true thiserror.workspace = true diff --git a/crates/synth-backend-riscv/Cargo.toml b/crates/synth-backend-riscv/Cargo.toml index e01ee4b..ab3206c 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.14.0" } -synth-synthesis = { path = "../synth-synthesis", version = "0.14.0" } +synth-core = { path = "../synth-core", version = "0.15.0" } +synth-synthesis = { path = "../synth-synthesis", version = "0.15.0" } 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 935c4bc..a25ffc0 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.14.0" } +synth-core = { path = "../synth-core", version = "0.15.0" } anyhow.workspace = true thiserror.workspace = true diff --git a/crates/synth-backend/Cargo.toml b/crates/synth-backend/Cargo.toml index 4cf1ecc..1d1e809 100644 --- a/crates/synth-backend/Cargo.toml +++ b/crates/synth-backend/Cargo.toml @@ -15,7 +15,7 @@ default = ["arm-cortex-m"] arm-cortex-m = ["synth-synthesis"] [dependencies] -synth-core = { path = "../synth-core", version = "0.14.0" } -synth-synthesis = { path = "../synth-synthesis", version = "0.14.0", optional = true } +synth-core = { path = "../synth-core", version = "0.15.0" } +synth-synthesis = { path = "../synth-synthesis", version = "0.15.0", optional = true } anyhow.workspace = true thiserror.workspace = true diff --git a/crates/synth-backend/src/arm_backend.rs b/crates/synth-backend/src/arm_backend.rs index 3f05142..34b4d84 100644 --- a/crates/synth-backend/src/arm_backend.rs +++ b/crates/synth-backend/src/arm_backend.rs @@ -492,11 +492,10 @@ fn compile_wasm_to_arm( // stack selector materialized into a scratch register (`movw rM,#C; lsl rD,rN,rM`) // folds to the immediate form (`lsl rD,rN,#C`), removing the dead `movw` — −1 // instruction, −1 live register. Removal-only (offset-neutral before branch - // resolution, like the dead-store pass). BEHIND `SYNTH_IMM_SHIFT_FOLD=1` - // (opt-in, off by default ⇒ bit-identical) while it earns the execution - // differential + gale's G474RE DWT gate — the same gated path local promotion - // and cmp→select took before shipping default-on. gale-named lever toward ≤1.3×. - let arm_instrs = if std::env::var("SYNTH_IMM_SHIFT_FOLD").is_ok() { + // resolution, like the dead-store pass). DEFAULT-ON as of v0.15.0: validated + // bit-identical results + a net cycle win on the dissolved hot path (−2 + // cyc/call, .text 100→90 B on gust_mix). Escape hatch: `SYNTH_NO_IMM_SHIFT_FOLD=1`. + let arm_instrs = if std::env::var("SYNTH_NO_IMM_SHIFT_FOLD").is_err() { let (out, folds) = synth_synthesis::liveness::fold_immediate_shifts(&arm_instrs); if std::env::var("SYNTH_FUSE_STATS").is_ok() { eprintln!( diff --git a/crates/synth-cli/Cargo.toml b/crates/synth-cli/Cargo.toml index 279bbc1..c8df4a0 100644 --- a/crates/synth-cli/Cargo.toml +++ b/crates/synth-cli/Cargo.toml @@ -27,18 +27,18 @@ verify = ["synth-verify"] # 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.14.0" } -synth-frontend = { path = "../synth-frontend", version = "0.14.0" } -synth-synthesis = { path = "../synth-synthesis", version = "0.14.0" } -synth-backend = { path = "../synth-backend", version = "0.14.0" } +synth-core = { path = "../synth-core", version = "0.15.0" } +synth-frontend = { path = "../synth-frontend", version = "0.15.0" } +synth-synthesis = { path = "../synth-synthesis", version = "0.15.0" } +synth-backend = { path = "../synth-backend", version = "0.15.0" } # Optional external backends -synth-backend-awsm = { path = "../synth-backend-awsm", version = "0.14.0", optional = true } -synth-backend-wasker = { path = "../synth-backend-wasker", version = "0.14.0", optional = true } -synth-backend-riscv = { path = "../synth-backend-riscv", version = "0.14.0", optional = true } +synth-backend-awsm = { path = "../synth-backend-awsm", version = "0.15.0", optional = true } +synth-backend-wasker = { path = "../synth-backend-wasker", version = "0.15.0", optional = true } +synth-backend-riscv = { path = "../synth-backend-riscv", version = "0.15.0", optional = true } # Optional verification (requires z3) -synth-verify = { path = "../synth-verify", version = "0.14.0", optional = true, features = ["z3-solver", "arm"] } +synth-verify = { path = "../synth-verify", version = "0.15.0", optional = true, features = ["z3-solver", "arm"] } # Optional PulseEngine WASM optimizer # Uncomment when loom crate is available: diff --git a/crates/synth-cli/tests/frozen_codegen_bytes.rs b/crates/synth-cli/tests/frozen_codegen_bytes.rs index 4b0e639..b2fd3cb 100644 --- a/crates/synth-cli/tests/frozen_codegen_bytes.rs +++ b/crates/synth-cli/tests/frozen_codegen_bytes.rs @@ -68,6 +68,7 @@ fn text_sha256(wasm: &str, backend: &str, target: &str) -> (String, usize) { let out = Command::new(synth()) .env_remove("SYNTH_NO_CMP_SELECT_FUSE") .env_remove("SYNTH_NO_LOCAL_PROMOTE") + .env_remove("SYNTH_NO_IMM_SHIFT_FOLD") .env_remove("SYNTH_CONST_CSE") .args([ "compile", @@ -129,33 +130,33 @@ fn assert_frozen(cases: &[(&str, &str, usize)], backend: &str, target: &str) { /// the `.py` differentials cover): control_step ↔ 0x00210A55, flight_seam_flat ↔ /// flat+inlined flight_algo 0x07FDF307, plus flight_seam and the div seam. /// -/// Goldens RE-FROZEN for v0.14.0 (#390): local promotion is now default-on (on top -/// of v0.13.0 cmp→select), so these lock the PROMOTED+FUSED .text. The execution -/// RESULTS are preserved — re-verified on this commit with both default-on: -/// control_step still 0x00210A55 (control_step_differential.py 13/13), flat+inlined -/// flight_algo still 0x07FDF307 (flight_seam_differential.py MATCH). .text shrank -/// again (stack spill/reloads eliminated): control_step 324→316, flight_seam -/// 902→866, flight_seam_flat 1122→1006 (−154 B total); signed_div_const (no -/// promotable i32 locals) unchanged. gale G474RE DWT: gust_mix 58→50 cyc/call -/// (−14%), 5→0 [sp] traffic. Prior cmp→select-only goldens were on main @ 377b93e -/// (v0.13.0), 2026-06-24. +/// Goldens RE-FROZEN for v0.15.0 (#390): immediate-shift folding is now default-on +/// (on top of v0.13.0 cmp→select + v0.14.0 local promotion), so these lock the +/// folded+promoted+fused .text. The execution RESULTS are preserved — re-verified +/// on this commit: control_step still 0x00210A55 (control_step_differential.py +/// 13/13), flat+inlined flight_algo still 0x07FDF307 (flight_seam_differential.py +/// MATCH). .text shrank again (constant shift-amount `movw`s removed): control_step +/// 316→304, flight_seam 866→774, flight_seam_flat 1006→910 (−200 B total); +/// signed_div_const (no register-shift folds) unchanged. Measured −2 cyc/call on the +/// dissolved hot path (.text 100→90 B on gust_mix). Prior promotion goldens were on +/// main @ 6b46f09 (v0.14.0), 2026-06-24. #[test] fn frozen_fixtures_text_is_bit_identical_oracle_001() { let cases = [ ( "control_step.wasm", - "cd929e7d91a8f7aad93f0e1cf0c93ecf3ccc6584ee94fb32e68d591134ed1410", - 316usize, + "1a97711cfb4754794a8577814388f08b81eff444edcba3de7d3e3d18ff435183", + 304usize, ), ( "flight_seam.wasm", - "52b19365e32bcd9d5a4be74565d0fa467517eb3a07648a3e1ccd0a67556c1948", - 866, + "9e73eea3867ba085820329951e84a7d650c38a7fc78d9d03a6a83d02963f9670", + 774, ), ( "flight_seam_flat.wasm", - "fa019f18cbc93869fff51630c6ab9cff6c4e052e22d783fe741b663ece49fa1e", - 1006, + "887ea546429a4569112147fdc94b0ba90f02a6ccd2b511aa2ca48dab017dbc2c", + 910, ), ( "signed_div_const.wasm", diff --git a/crates/synth-frontend/Cargo.toml b/crates/synth-frontend/Cargo.toml index 7489eaa..820b6ed 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.14.0" } +synth-core = { path = "../synth-core", version = "0.15.0" } wasmparser.workspace = true wasm-encoder.workspace = true diff --git a/crates/synth-opt/Cargo.toml b/crates/synth-opt/Cargo.toml index b553806..7f2dbb6 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.14.0" } +synth-cfg = { path = "../synth-cfg", version = "0.15.0" } [dev-dependencies] criterion = { version = "0.8", features = ["html_reports"] } diff --git a/crates/synth-synthesis/Cargo.toml b/crates/synth-synthesis/Cargo.toml index 23bd273..7c1a072 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.14.0" } -synth-cfg = { path = "../synth-cfg", version = "0.14.0" } -synth-opt = { path = "../synth-opt", version = "0.14.0" } +synth-core = { path = "../synth-core", version = "0.15.0" } +synth-cfg = { path = "../synth-cfg", version = "0.15.0" } +synth-opt = { path = "../synth-opt", version = "0.15.0" } serde.workspace = true anyhow.workspace = true thiserror.workspace = true diff --git a/crates/synth-verify/Cargo.toml b/crates/synth-verify/Cargo.toml index b0065f3..e3204cb 100644 --- a/crates/synth-verify/Cargo.toml +++ b/crates/synth-verify/Cargo.toml @@ -17,12 +17,12 @@ arm = ["synth-synthesis"] [dependencies] # Core dependencies (always required) -synth-core = { path = "../synth-core", version = "0.14.0" } -synth-cfg = { path = "../synth-cfg", version = "0.14.0" } -synth-opt = { path = "../synth-opt", version = "0.14.0" } +synth-core = { path = "../synth-core", version = "0.15.0" } +synth-cfg = { path = "../synth-cfg", version = "0.15.0" } +synth-opt = { path = "../synth-opt", version = "0.15.0" } # ARM synthesis (optional, behind 'arm' feature) -synth-synthesis = { path = "../synth-synthesis", version = "0.14.0", optional = true } +synth-synthesis = { path = "../synth-synthesis", version = "0.15.0", optional = true } # SMT solver for formal verification z3 = { version = "0.19", features = ["static-link-z3"], optional = true }