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 {
compile_wcet_hinted(wat, triple, None)
}
fn compile_wcet_hinted(wat: &str, triple: &str, hints_json: Option<&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 mut args = vec![
"compile".to_string(),
wat_path.to_str().unwrap().to_string(),
"-o".to_string(),
out_path.to_str().unwrap().to_string(),
"-t".to_string(),
triple.to_string(),
"--emit-wcet".to_string(),
];
if let Some(h) = hints_json {
let hints_path = dir.join("hints.json");
std::fs::write(&hints_path, h).unwrap();
args.push("--wcet-hints".to_string());
args.push(hints_path.to_str().unwrap().to_string());
}
let status = Command::new(synth())
.args(&args)
.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 data_dependent_loop_still_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"
);
}
#[test]
fn const_bound_loop_is_bounded_with_static_trip() {
let wat = r#"
(module
(func (export "sum10") (result i32)
(local i32 i32)
(block
(loop
local.get 0 i32.const 10 i32.lt_s i32.eqz br_if 1
local.get 1 local.get 0 i32.add local.set 1
local.get 0 i32.const 1 i32.add local.set 0
br 0))
local.get 1))
"#;
let report = compile_wcet(wat, "cortex-m4");
assert_bounded(&report, "sum10", 349);
assert_loop(&report, "sum10", 0, 10, "static");
assert_trip_floor(&report, "sum10");
}
#[test]
fn bottom_test_loop_is_bounded() {
let wat = r#"
(module
(func (export "bottom") (result i32)
(local i32 i32)
(loop
local.get 1 local.get 0 i32.add local.set 1
local.get 0 i32.const 1 i32.add local.tee 0
i32.const 10 i32.lt_s br_if 0)
local.get 1))
"#;
let report = compile_wcet(wat, "cortex-m4");
assert_bounded(&report, "bottom", 229);
assert_loop(&report, "bottom", 0, 10, "static");
assert_trip_floor(&report, "bottom");
}
#[test]
fn nested_const_loops_bound_multiplicatively() {
let wat = r#"
(module
(func (export "nested") (result i32)
(local i32 i32 i32)
(block
(loop
local.get 0 i32.const 5 i32.lt_s i32.eqz br_if 1
i32.const 0 local.set 1
(block
(loop
local.get 1 i32.const 3 i32.lt_s i32.eqz br_if 1
local.get 2 i32.const 1 i32.add local.set 2
local.get 1 i32.const 1 i32.add local.set 1
br 0))
local.get 0 i32.const 1 i32.add local.set 0
br 0))
local.get 2))
"#;
let report = compile_wcet(wat, "cortex-m4");
assert_bounded(&report, "nested", 863);
assert_loop(&report, "nested", 0, 5, "static");
assert_loop(&report, "nested", 1, 3, "static");
assert_trip_floor(&report, "nested");
}
#[test]
fn memory_writing_const_loop_is_bounded() {
let wat = r#"
(module
(func (export "memloop") (result i32)
(local i32)
(block
(loop
local.get 0 i32.const 16 i32.lt_s i32.eqz br_if 1
local.get 0 i32.const 4 i32.mul
local.get 0
i32.store
local.get 0 i32.const 1 i32.add local.set 0
br 0))
i32.const 44 i32.load)
(memory 1))
"#;
let report = compile_wcet(wat, "cortex-m4");
let f = func(&report, "memloop");
assert_eq!(
f.get("status").and_then(Value::as_str),
Some("bounded"),
"memory-writing const-bound loop must bound: {f}"
);
assert_loop(&report, "memloop", 0, 16, "static");
assert_trip_floor(&report, "memloop");
}
#[test]
fn zero_trip_loop_is_bounded() {
let wat = r#"
(module
(func (export "trip0") (result i32)
(local i32 i32)
(block
(loop
local.get 0 i32.const 0 i32.lt_s i32.eqz br_if 1
local.get 1 i32.const 1 i32.add local.set 1
local.get 0 i32.const 1 i32.add local.set 0
br 0))
local.get 1))
"#;
let report = compile_wcet(wat, "cortex-m4");
assert_loop(&report, "trip0", 0, 0, "static");
assert_trip_floor(&report, "trip0");
}
#[test]
fn conditional_counter_store_still_declines() {
let wat = r#"
(module
(func (export "condstore") (param i32) (result i32)
(local i32)
(block
(loop
local.get 1 i32.const 10 i32.lt_s i32.eqz br_if 1
(if (local.get 0)
(then local.get 1 i32.const 5 i32.add local.set 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, "condstore", "loop");
}
const EQEXIT_WAT: &str = r#"
(module
(func (export "eqexit") (result i32)
(local i32 i32)
(block
(loop
local.get 0 i32.const 8 i32.eq br_if 1
local.get 1 local.get 0 i32.add local.set 1
local.get 0 i32.const 1 i32.add local.set 0
br 0))
local.get 1))
"#;
#[test]
fn wrong_hint_below_real_trip_is_rejected_red_first() {
let hints = r#"{"schema":"synth-wcet-hints-v1","functions":{"eqexit":{"loop_bounds":[3]}}}"#;
let report = compile_wcet_hinted(EQEXIT_WAT, "cortex-m4", Some(hints));
assert_declined(&report, "eqexit", "loop");
let f = func(&report, "eqexit");
let rej = f
.get("hint_rejections")
.and_then(Value::as_array)
.and_then(|a| a.first())
.unwrap_or_else(|| panic!("wrong hint must be RECORDED as rejected: {f}"));
assert_eq!(
rej.get("reason").and_then(Value::as_str),
Some("hint-below-derived-trip"),
"wrong hint must carry the specific machine rejection reason: {rej}"
);
assert_eq!(rej.get("hint").and_then(Value::as_u64), Some(3));
}
#[test]
fn equality_exit_unhinted_still_declines() {
let report = compile_wcet(EQEXIT_WAT, "cortex-m4");
assert_declined(&report, "eqexit", "loop");
}
#[test]
fn correct_hint_converts_decline_to_bound() {
let hints = r#"{"schema":"synth-wcet-hints-v1","functions":{"eqexit":{"loop_bounds":[8]}}}"#;
let report = compile_wcet_hinted(EQEXIT_WAT, "cortex-m4", Some(hints));
assert_bounded(&report, "eqexit", 254);
assert_loop(&report, "eqexit", 0, 8, "hint-verified");
assert_trip_floor(&report, "eqexit");
}
#[test]
fn wrong_hint_on_static_loop_bound_stands_rejection_recorded() {
let wat = r#"
(module
(func (export "sum10") (result i32)
(local i32 i32)
(block
(loop
local.get 0 i32.const 10 i32.lt_s i32.eqz br_if 1
local.get 1 local.get 0 i32.add local.set 1
local.get 0 i32.const 1 i32.add local.set 0
br 0))
local.get 1))
"#;
let hints = r#"{"schema":"synth-wcet-hints-v1","functions":{"sum10":{"loop_bounds":[5]}}}"#;
let report = compile_wcet_hinted(wat, "cortex-m4", Some(hints));
assert_bounded(&report, "sum10", 349); assert_loop(&report, "sum10", 0, 10, "static");
let f = func(&report, "sum10");
let rej = f
.get("hint_rejections")
.and_then(Value::as_array)
.and_then(|a| a.first())
.unwrap_or_else(|| panic!("contradicting hint must be recorded: {f}"));
assert_eq!(
rej.get("reason").and_then(Value::as_str),
Some("hint-below-derived-trip")
);
}
#[test]
fn data_dependent_hint_is_rejected_unverifiable() {
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 hints = r#"{"schema":"synth-wcet-hints-v1","functions":{"spin":{"loop_bounds":[100]}}}"#;
let report = compile_wcet_hinted(wat, "cortex-m4", Some(hints));
assert_declined(&report, "spin", "loop");
let f = func(&report, "spin");
let rej = f
.get("hint_rejections")
.and_then(Value::as_array)
.and_then(|a| a.first())
.unwrap_or_else(|| panic!("unverifiable hint must be RECORDED as rejected: {f}"));
assert_eq!(
rej.get("reason").and_then(Value::as_str),
Some("hint-unverifiable-induction"),
"data-dependent bound: hint must be rejected as unverifiable: {rej}"
);
}
#[test]
fn hints_cli_misuse_fails_loudly() {
let dir = std::env::temp_dir().join(format!(
"synth_wcet_gate_cli_{}_{}",
std::process::id(),
unique_id()
));
std::fs::create_dir_all(&dir).unwrap();
let wat_path = dir.join("f.wat");
std::fs::write(
&wat_path,
r#"(module (func (export "k") (result i32) i32.const 1))"#,
)
.unwrap();
let hints_path = dir.join("hints.json");
std::fs::write(
&hints_path,
r#"{"schema":"synth-wcet-hints-v1","functions":{}}"#,
)
.unwrap();
let out = dir.join("f.elf");
let status = Command::new(synth())
.args([
"compile",
wat_path.to_str().unwrap(),
"-o",
out.to_str().unwrap(),
"-t",
"cortex-m4",
"--wcet-hints",
hints_path.to_str().unwrap(),
])
.status()
.unwrap();
assert!(
!status.success(),
"--wcet-hints without --emit-wcet must fail"
);
std::fs::write(&hints_path, "{not json").unwrap();
let status = Command::new(synth())
.args([
"compile",
wat_path.to_str().unwrap(),
"-o",
out.to_str().unwrap(),
"-t",
"cortex-m4",
"--emit-wcet",
"--wcet-hints",
hints_path.to_str().unwrap(),
])
.status()
.unwrap();
assert!(!status.success(), "malformed --wcet-hints must fail loudly");
std::fs::write(&hints_path, r#"{"schema":"bogus-v9","functions":{}}"#).unwrap();
let status = Command::new(synth())
.args([
"compile",
wat_path.to_str().unwrap(),
"-o",
out.to_str().unwrap(),
"-t",
"cortex-m4",
"--emit-wcet",
"--wcet-hints",
hints_path.to_str().unwrap(),
])
.status()
.unwrap();
assert!(!status.success(), "wrong hints schema must fail loudly");
}
fn assert_loop(report: &Value, name: &str, idx: usize, trip: u64, source: &str) {
let f = func(report, name);
let l = f
.get("loops")
.and_then(Value::as_array)
.and_then(|a| a.get(idx))
.unwrap_or_else(|| panic!("{name}: no loop record #{idx} (entry: {f})"));
assert_eq!(
l.get("trip_count").and_then(Value::as_u64),
Some(trip),
"{name} loop {idx}: trip count drifted (record: {l})"
);
assert_eq!(
l.get("source").and_then(Value::as_str),
Some(source),
"{name} loop {idx}: wrong bound source (record: {l})"
);
}
fn assert_trip_floor(report: &Value, name: &str) {
let f = func(report, name);
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,
"{name}: bound {cycles} < instr_count {instrs} — unsound"
);
for l in f
.get("loops")
.and_then(Value::as_array)
.into_iter()
.flatten()
{
let trip = l.get("trip_count").and_then(Value::as_u64).unwrap();
let region = l.get("region_instr_count").and_then(Value::as_u64).unwrap();
assert!(
cycles >= trip.saturating_mul(region),
"{name}: bound {cycles} < trip {trip} × region {region} — the loop's \
instructions alone execute more times than the bound allows: unsound"
);
}
}