use std::process::Command;
use serde_json::Value;
fn synth() -> &'static str {
env!("CARGO_BIN_EXE_synth")
}
fn unique_id() -> u64 {
use std::sync::atomic::{AtomicU64, Ordering};
static N: AtomicU64 = AtomicU64::new(0);
N.fetch_add(1, Ordering::Relaxed)
}
fn compile_wcet(wat: &str, triple: &str) -> Value {
let dir = std::env::temp_dir().join(format!(
"synth_wcet_gate_{}_{}_{}",
std::process::id(),
triple.replace(['/', '-'], "_"),
unique_id(),
));
std::fs::create_dir_all(&dir).unwrap();
let wat_path = dir.join("f.wat");
std::fs::write(&wat_path, wat).unwrap();
let out_path = dir.join("f.elf");
let status = Command::new(synth())
.args([
"compile",
wat_path.to_str().unwrap(),
"-o",
out_path.to_str().unwrap(),
"-t",
triple,
"--emit-wcet",
])
.status()
.expect("failed to run synth compile");
assert!(status.success(), "synth compile failed for triple {triple}");
let sidecar = {
let mut s = out_path.into_os_string();
s.push(".wcet.json");
std::path::PathBuf::from(s)
};
let json = std::fs::read_to_string(&sidecar)
.unwrap_or_else(|e| panic!("no wcet sidecar at {}: {e}", sidecar.display()));
serde_json::from_str(&json).expect("sidecar is not valid JSON")
}
fn func<'a>(report: &'a Value, name: &str) -> &'a Value {
report
.get("functions")
.and_then(Value::as_array)
.expect("functions array")
.iter()
.find(|f| f.get("name").and_then(Value::as_str) == Some(name))
.unwrap_or_else(|| panic!("no function named {name} in report"))
}
fn assert_bounded(report: &Value, name: &str, expected_cycles: u64) {
let f = func(report, name);
assert_eq!(
f.get("status").and_then(Value::as_str),
Some("bounded"),
"{name}: expected bounded, got {f}"
);
assert_eq!(
f.get("cycles").and_then(Value::as_u64),
Some(expected_cycles),
"{name}: WCET cycles drifted — a table change altered the bound. Re-derive \
against the Cortex-M3/M4 TRM and update BOTH the literal here and claims.yaml. \
(entry: {f})"
);
}
fn assert_declined(report: &Value, name: &str, reason: &str) {
let f = func(report, name);
assert_eq!(
f.get("status").and_then(Value::as_str),
Some("declined"),
"{name}: expected declined ({reason}), got a bound: {f}"
);
assert_eq!(
f.get("reason").and_then(Value::as_str),
Some(reason),
"{name}: wrong decline reason (entry: {f})"
);
}
#[test]
fn loop_free_add3_is_bounded_exact() {
let wat = r#"
(module
(func (export "add3") (param i32 i32 i32) (result i32)
local.get 0 local.get 1 i32.add local.get 2 i32.add))
"#;
let report = compile_wcet(wat, "cortex-m4");
assert_bounded(&report, "add3", 19);
let f = func(&report, "add3");
let cycles = f.get("cycles").and_then(Value::as_u64).unwrap();
let instrs = f.get("instr_count").and_then(Value::as_u64).unwrap();
assert!(
cycles >= instrs,
"add3: bound {cycles} < instr_count {instrs} — unsound"
);
}
#[test]
fn loop_free_const_exact_literal() {
let wat = r#"
(module
(func (export "k") (result i32) i32.const 7))
"#;
let report = compile_wcet(wat, "cortex-m4");
let f = func(&report, "k");
assert_eq!(
f.get("status").and_then(Value::as_str),
Some("bounded"),
"const fn must be loop-free bounded: {f}"
);
let cycles = f.get("cycles").and_then(Value::as_u64).unwrap();
let instrs = f.get("instr_count").and_then(Value::as_u64).unwrap();
assert!(
cycles >= instrs,
"const: bound {cycles} < instr_count {instrs}"
);
assert!(
cycles >= 5,
"const: bound {cycles} < 5 — a loop-free fn with a MOV + return path costs \
at least a MOV (1) + a branch/POP-to-PC (>=4); a lower bound is unsound"
);
}
#[test]
fn loop_free_if_else_is_bounded() {
let wat = r#"
(module
(func (export "sel") (param i32 i32 i32) (result i32)
local.get 0
(if (result i32)
(then local.get 1)
(else local.get 2))))
"#;
let report = compile_wcet(wat, "cortex-m4");
let f = func(&report, "sel");
assert_eq!(
f.get("status").and_then(Value::as_str),
Some("bounded"),
"an if/else with a FORWARD branch is loop-free and must be bounded (summing \
both arms over-approximates the max — sound): {f}"
);
let cycles = f.get("cycles").and_then(Value::as_u64).unwrap();
let instrs = f.get("instr_count").and_then(Value::as_u64).unwrap();
assert!(
cycles >= instrs,
"sel: bound {cycles} < instr_count {instrs} — unsound"
);
}
#[test]
fn loop_declines_with_loop_reason() {
let wat = r#"
(module
(func (export "spin") (param i32) (result i32)
(local i32)
(block
(loop
local.get 1 local.get 0 i32.lt_s i32.eqz br_if 1
local.get 1 i32.const 1 i32.add local.set 1
br 0))
local.get 1))
"#;
let report = compile_wcet(wat, "cortex-m4");
assert_declined(&report, "spin", "loop");
}
#[test]
fn call_declines_with_call_reason() {
let wat = r#"
(module
(func $leaf (param i32) (result i32) local.get 0 i32.const 1 i32.add)
(func (export "caller") (param i32) (result i32)
local.get 0 call $leaf))
"#;
let report = compile_wcet(wat, "cortex-m4");
assert_declined(&report, "caller", "call");
}
#[test]
fn i64_div_declines_with_looped_expansion_reason() {
let wat = r#"
(module
(func (export "d") (param i64 i64) (result i64)
local.get 0 local.get 1 i64.div_u))
"#;
let report = compile_wcet(wat, "cortex-m4");
assert_declined(&report, "d", "looped-expansion");
}
#[test]
fn m7_declines_unsupported_core() {
let wat = r#"
(module (func (export "add3") (param i32 i32 i32) (result i32)
local.get 0 local.get 1 i32.add local.get 2 i32.add))
"#;
let report = compile_wcet(wat, "cortex-m7");
assert_declined(&report, "add3", "unsupported-core");
}
#[test]
fn m4f_declines_unsupported_core_ambiguous_triple() {
let wat = r#"
(module (func (export "add3") (param i32 i32 i32) (result i32)
local.get 0 local.get 1 i32.add local.get 2 i32.add))
"#;
let report = compile_wcet(wat, "cortex-m4f");
assert_declined(&report, "add3", "unsupported-core");
}
#[test]
fn report_carries_precondition() {
let wat = r#"(module (func (export "k") (result i32) i32.const 1))"#;
let report = compile_wcet(wat, "cortex-m4");
assert_eq!(
report.get("schema").and_then(Value::as_str),
Some("synth-wcet-v1")
);
assert_eq!(
report.get("wait_states").and_then(Value::as_u64),
Some(0),
"the sound table is zero-wait-state; the precondition must say so"
);
assert!(
report
.get("memory_assumption")
.and_then(Value::as_str)
.is_some_and(|s| s.contains("zero-wait-state")),
"the bound is conditional on a memory precondition that must be recorded"
);
}