synth-cli 0.53.0

CLI for Synth, the WebAssembly-to-ARM Cortex-M AOT compiler
//! #735 — CI pin for the synth-side byte numbers in the parity benchmark
//! (`artifacts/parity-benchmark.md`, regenerated by
//! `scripts/repro/parity_benchmark/run.py --report`).
//!
//! The benchmark's prose says "AOT-wasm ≥ native today on certified-elision
//! shapes; general regalloc still ~3.5x" — this test makes those MEASURED
//! numbers machine-checked so the document cannot drift from measurement:
//! a size regression reddens CI, and a size WIN reddens it too, forcing a
//! deliberate repin + report regeneration (the pin going DOWN is the visible
//! evidence a perf lane landed). Same method and contract as
//! `size_attribution_390.rs` (which already pins the gust_kernel functions —
//! not duplicated here).
//!
//! Pinned rows (per-function `.text`, sorted symbol-address deltas):
//!   * gust_mix clamp shape, default path        — 84 B
//!   * 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     — 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
//! arm-none-eabi-gcc, not synth; run.py re-measures them and the .md carries
//! their provenance.

use std::path::{Path, PathBuf};
use std::process::Command;

use object::{Object, ObjectSection, ObjectSymbol, SymbolKind};

fn synth() -> &'static str {
    env!("CARGO_BIN_EXE_synth")
}

fn repro(rel: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../..")
        .join("scripts/repro")
        .join(rel)
}

fn wat_fixture(rel: &str) -> Vec<u8> {
    let wat = std::fs::read(repro(rel)).expect("read fixture wat");
    wat::parse_bytes(&wat)
        .expect("fixture wat must parse")
        .into_owned()
}

/// Compile on the shipped default (optimized) path and return per-function
/// `.text` sizes by symbol name — sorted-address deltas (next symbol /
/// section end), the same symtab method as `size_attribution_390.rs`.
fn per_function_sizes(wasm: &[u8], tag: &str, target: &str, fact_spec: bool) -> Vec<(String, u64)> {
    let dir = std::env::temp_dir().join("parity_benchmark_735");
    std::fs::create_dir_all(&dir).expect("mk tempdir");
    let input = dir.join(format!("{tag}.wasm"));
    let elf = dir.join(format!("{tag}.o"));
    std::fs::write(&input, wasm).expect("write wasm");

    let mut cmd = Command::new(synth());
    cmd.args([
        "compile",
        input.to_str().unwrap(),
        "-o",
        elf.to_str().unwrap(),
        "-b",
        "arm",
        "--target",
        target,
        "--all-exports",
    ]);
    if fact_spec {
        cmd.env("SYNTH_FACT_SPEC", "1");
    } else {
        cmd.env_remove("SYNTH_FACT_SPEC");
    }
    let out = cmd.output().expect("run synth");
    assert!(
        out.status.success(),
        "synth compile of '{tag}' failed: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    if fact_spec {
        // Non-vacuity: the 14 B pin is only meaningful if BOTH clamp elisions
        // were actually certificate-admitted (a solver-less binary would
        // silently emit the 84 B general lowering).
        let stderr = String::from_utf8_lossy(&out.stderr);
        assert_eq!(
            stderr.matches("fact-spec: ADMIT").count(),
            2,
            "expected 2 ordeal-certified elisions; stderr:\n{stderr}"
        );
    }

    let bytes = std::fs::read(&elf).expect("read elf");
    let obj = object::File::parse(&*bytes).expect("parse elf");
    let text = obj.section_by_name(".text").expect(".text");
    let end = text.address() + text.size();

    let mut starts: Vec<(u64, String)> = obj
        .symbols()
        .filter(|s| {
            !s.name().unwrap_or("").is_empty()
                && matches!(
                    s.kind(),
                    SymbolKind::Text | SymbolKind::Label | SymbolKind::Unknown
                )
                && s.address() >= text.address()
                && s.address() < end
        })
        // Thumb function symbols carry the interworking bit (addr|1) in the
        // symtab; mask it so the LAST function's size (next = section end)
        // is not off by one. objdump masks this too; the delta method in
        // size_attribution_390.rs never pins a last-in-section function, so
        // it does not hit this.
        .map(|s| (s.address() & !1, s.name().unwrap().to_string()))
        .collect();
    starts.sort();
    starts.dedup_by(|a, b| a.0 == b.0);

    starts
        .iter()
        .enumerate()
        .map(|(i, (addr, name))| {
            let next = starts.get(i + 1).map(|(a, _)| *a).unwrap_or(end);
            (name.clone(), next - addr)
        })
        .collect()
}

fn assert_pinned(sizes: &[(String, u64)], name: &str, locked: u64) {
    let got = sizes
        .iter()
        .find(|(n, _)| n == name)
        .unwrap_or_else(|| panic!("function {name} not found in .text"))
        .1;
    assert_eq!(
        got,
        locked,
        "{name}: {got} B (locked {locked} B, delta {:+}) — the #735 parity \
         benchmark pins this MEASURED number. If a change intentionally moved \
         it, repin here AND regenerate the report: \
         SYNTH=$CARGO_TARGET_DIR/debug/synth \
         python3 scripts/repro/parity_benchmark/run.py --report",
        got as i64 - locked as i64
    );
}

/// gust_mix clamp shape (scripts/repro/fact_spec_clamp_494.wat), default
/// path, no facts: the general lowering the report's 3.23x-vs-gcc row cites.
#[test]
fn clamp_default_path_is_pinned_735() {
    let wasm = wat_fixture("fact_spec_clamp_494.wat");
    let sizes = per_function_sizes(&wasm, "clamp_default", "cortex-m4", false);
    assert_pinned(&sizes, "gust_mix", 84);
}

/// flat_flight (loom-dissolved composed kernel, in-tree C twin), default
/// path: the 2.54x-vs-gcc row.
#[test]
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);
    // 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
/// 1.64x-vs-gcc row.
#[test]
fn falcon_axis_is_pinned_735() {
    let wasm = wat_fixture("parity_benchmark/falcon_axis.wat");
    let sizes = per_function_sizes(&wasm, "falcon_axis", "cortex-m4f", false);
    assert_pinned(&sizes, "falcon_axis", 72);
}

/// THE HEADLINE ROW: gust_mix clamp under the loom-proven premise
/// ch ∈ [524, 1524] — both branches ordeal-certificate-elided, 14 B, smaller
/// than the measured 26 B arm-none-eabi-gcc -Os twin. Needs the verify
/// feature (solver-carrying binary); the fact-spec CI job runs it via
/// `cargo test -p synth-cli --features verify --test parity_benchmark_735`.
#[cfg(feature = "verify")]
#[test]
fn clamp_fact_spec_is_pinned_735() {
    // Schema-v1 wsc.facts value-range record on (func 0, value_id 0) —
    // identical encoding to fact_spec_clamp_494.rs.
    fn leb_u32(mut v: u32, out: &mut Vec<u8>) {
        loop {
            let mut b = (v & 0x7f) as u8;
            v >>= 7;
            if v != 0 {
                b |= 0x80;
            }
            out.push(b);
            if v == 0 {
                return;
            }
        }
    }
    fn leb_s64(mut v: i64, out: &mut Vec<u8>) {
        loop {
            let mut b = (v & 0x7f) as u8;
            v >>= 7;
            let done = (v == 0 && b & 0x40 == 0) || (v == -1 && b & 0x40 != 0);
            if !done {
                b |= 0x80;
            }
            out.push(b);
            if done {
                return;
            }
        }
    }

    let mut wasm = wat_fixture("fact_spec_clamp_494.wat");
    let mut body = Vec::new();
    leb_s64(524, &mut body);
    leb_s64(1524, &mut body);
    let mut payload = vec![0x01]; // version
    leb_u32(1, &mut payload); // count
    payload.push(0x01); // kind: value-range
    leb_u32(0, &mut payload); // func_index
    leb_u32(0, &mut payload); // value_id
    leb_u32(body.len() as u32, &mut payload);
    payload.extend_from_slice(&body);
    let name = b"wsc.facts";
    let mut content = Vec::new();
    leb_u32(name.len() as u32, &mut content);
    content.extend_from_slice(name);
    content.extend_from_slice(&payload);
    wasm.push(0x00);
    leb_u32(content.len() as u32, &mut wasm);
    wasm.extend_from_slice(&content);

    let sizes = per_function_sizes(&wasm, "clamp_spec", "cortex-m4", true);
    assert_pinned(&sizes, "gust_mix", 14);
}