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_relocatable(wat: &str, triple: &str) -> Value {
compile_wcet_inner(wat, triple, None, true)
}
fn compile_wcet_hinted(wat: &str, triple: &str, hints_json: Option<&str>) -> Value {
compile_wcet_inner(wat, triple, hints_json, false)
}
fn compile_wcet_inner(
wat: &str,
triple: &str,
hints_json: Option<&str>,
relocatable: bool,
) -> 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 relocatable {
args.push("--relocatable".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 external_import_call_declines_with_call_reason() {
let wat = r#"
(module
(import "env" "ext" (func $ext (param i32) (result i32)))
(func (export "caller") (param i32) (result i32)
local.get 0 call $ext))
"#;
let report = compile_wcet_relocatable(wat, "cortex-m4");
assert_declined(&report, "caller", "call");
}
#[test]
fn direct_call_chain_composes_exact_bounds() {
let wat = r#"
(module
(func $leaf (param i32) (result i32) local.get 0 i32.const 1 i32.add)
(func $mid (param i32) (result i32) local.get 0 call $leaf i32.const 2 i32.add)
(func (export "root") (param i32) (result i32)
local.get 0 call $mid call $mid))
"#;
let report = compile_wcet(wat, "cortex-m4");
assert_bounded(&report, "leaf", 19);
assert_bounded(&report, "mid", 51);
assert_bounded(&report, "root", 136);
for name in ["leaf", "mid", "root"] {
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} < instrs {instrs}");
}
let root = func(&report, "root")
.get("cycles")
.and_then(Value::as_u64)
.unwrap();
let mid = func(&report, "mid")
.get("cycles")
.and_then(Value::as_u64)
.unwrap();
assert!(
root >= 2 * mid,
"root bound {root} must cover both mid-calls (2 × {mid})"
);
}
#[test]
fn direct_call_inside_proven_loop_counts_callee_per_trip() {
let wat = r#"
(module
(func $leaf (param i32) (result i32) local.get 0 i32.const 1 i32.add)
(func (export "loopcaller") (result i32)
(local i32 i32)
(block
(loop
local.get 0 i32.const 10 i32.lt_s i32.eqz br_if 1
local.get 1 call $leaf 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");
let leaf = func(&report, "leaf")
.get("cycles")
.and_then(Value::as_u64)
.unwrap();
assert_eq!(leaf, 19, "leaf body pins at 19");
let f = func(&report, "loopcaller");
assert_eq!(
f.get("status").and_then(Value::as_str),
Some("bounded"),
"a direct call inside a PROVEN loop must compose (callee counted trip×): {f}"
);
assert_loop(&report, "loopcaller", 0, 10, "static");
assert_trip_floor(&report, "loopcaller");
let cycles = f.get("cycles").and_then(Value::as_u64).unwrap();
assert!(
cycles >= 10 * leaf,
"loopcaller bound {cycles} < 10 × leaf {leaf} — the call-in-loop multiplier \
was lost; a callee in a trip-10 loop must be counted 10×, not once (unsound)"
);
}
#[test]
fn self_recursion_declines_with_recursion_reason() {
let wat = r#"
(module
(func $fac (export "fac") (param i32) (result i32)
local.get 0 i32.eqz
(if (result i32)
(then i32.const 1)
(else local.get 0 local.get 0 i32.const 1 i32.sub call $fac i32.mul))))
"#;
let report = compile_wcet(wat, "cortex-m4");
assert_declined(&report, "fac", "recursion");
}
#[test]
fn mutual_recursion_declines_both() {
let wat = r#"
(module
(func $ping (export "ping") (param i32) (result i32)
local.get 0 i32.eqz
(if (result i32)
(then i32.const 0)
(else local.get 0 i32.const 1 i32.sub call $pong)))
(func $pong (export "pong") (param i32) (result i32)
local.get 0 i32.eqz
(if (result i32)
(then i32.const 1)
(else local.get 0 i32.const 1 i32.sub call $ping))))
"#;
let report = compile_wcet(wat, "cortex-m4");
assert_declined(&report, "ping", "recursion");
assert_declined(&report, "pong", "recursion");
}
#[test]
fn indirect_call_declines_with_indirect_reason() {
let wat = r#"
(module
(type $t (func (param i32) (result i32)))
(table 1 funcref)
(func $g (param i32) (result i32) local.get 0 i32.const 1 i32.add)
(elem (i32.const 0) $g)
(func (export "dispatch") (param i32) (result i32)
local.get 0 i32.const 0 call_indirect (type $t)))
"#;
let report = compile_wcet(wat, "cortex-m4");
assert_declined(&report, "dispatch", "indirect-call");
}
#[test]
fn declined_callee_propagates_up_as_callee_unbounded() {
let wat = r#"
(module
(func $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)
(func (export "caller") (param i32) (result i32)
local.get 0 call $spin))
"#;
let report = compile_wcet(wat, "cortex-m4");
assert_declined(&report, "spin", "loop");
assert_declined(&report, "caller", "callee-unbounded");
}
#[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 proven_loop_containing_i64_rotl_stays_bounded() {
let wat = r#"
(module
(func (export "rot") (param i64) (result i64)
(local i32) (local i64)
(block
(loop
local.get 1 i32.const 8 i32.lt_s i32.eqz br_if 1
local.get 2 local.get 0 i64.const 3 i64.rotl i64.add local.set 2
local.get 1 i32.const 1 i32.add local.set 1
br 0))
local.get 2))
"#;
let report = compile_wcet(wat, "cortex-m4");
assert_sp_motion_loop_bounded(&report, "rot", 3120);
}
#[test]
fn proven_loop_containing_i64_popcnt_stays_bounded() {
let wat = r#"
(module
(func (export "pc") (param i64) (result i64)
(local i32) (local i64)
(block
(loop
local.get 1 i32.const 8 i32.lt_s i32.eqz br_if 1
local.get 2 local.get 0 i64.popcnt i64.add local.set 2
local.get 1 i32.const 1 i32.add local.set 1
br 0))
local.get 2))
"#;
let report = compile_wcet(wat, "cortex-m4");
assert_sp_motion_loop_bounded(&report, "pc", 4612);
}
#[test]
fn proven_loop_containing_sp_free_i64_op_is_bounded() {
let wat = r#"
(module
(func (export "andloop") (param i64) (result i64)
(local i32) (local i64)
(block
(loop
local.get 1 i32.const 8 i32.lt_s i32.eqz br_if 1
local.get 2 local.get 0 i64.const 3 i64.and i64.add local.set 2
local.get 1 i32.const 1 i32.add local.set 1
br 0))
local.get 2))
"#;
let report = compile_wcet(wat, "cortex-m4");
let f = func(&report, "andloop");
assert_eq!(
f.get("status").and_then(Value::as_str),
Some("bounded"),
"the SP-free control must be bounded: {f}"
);
}
fn assert_sp_motion_loop_bounded(report: &Value, name: &str, cycles: u64) {
let f = func(report, name);
assert_eq!(
f.get("status").and_then(Value::as_str),
Some("bounded"),
"#946: {name} must stay BOUNDED — `may_move_sp` answers `false` for the \
net-zero PUSH/POP expansions, earned by the WalkState non-negative-slot \
invariant. A decline here means that invariant or that arm moved: {f}"
);
assert_eq!(
f.get("cycles").and_then(Value::as_u64),
Some(cycles),
"#946: {name} bound changed (was {cycles}, the value main emitted before \
the wildcard was expanded): {f}"
);
let loops = f.get("loops").and_then(Value::as_array).expect("loops[]");
assert_eq!(loops.len(), 1, "{name}: expected exactly one proven loop");
assert_eq!(
loops[0].get("trip_count").and_then(Value::as_u64),
Some(8),
"{name}: trip count must still be the statically proven 8"
);
assert_eq!(
loops[0].get("source").and_then(Value::as_str),
Some("static"),
"{name}: the trip must be proven statically, not via a hint"
);
}
#[test]
fn i64_const_relocatable_leaf_is_bounded() {
let wat = r#"
(module
(func (export "k") (result i64)
i64.const 1000000))
"#;
let report = compile_wcet_relocatable(wat, "cortex-m4");
assert_bounded(&report, "k", 52);
}
#[test]
fn i64_str_relocatable_leaf_is_bounded() {
let wat = r#"
(module
(memory 1)
(func (export "st") (param i32)
local.get 0
i64.const 42
i64.store))
"#;
let report = compile_wcet_relocatable(wat, "cortex-m4");
assert_bounded(&report, "st", 72);
}
#[test]
fn i64_ldr_relocatable_leaf_is_bounded() {
let wat = r#"
(module
(memory 1)
(func (export "ld") (param i32) (result i64)
local.get 0
i64.load))
"#;
let report = compile_wcet_relocatable(wat, "cortex-m4");
assert_bounded(&report, "ld", 54);
}
#[test]
fn i64_ldr_cascade_composes_to_bounded() {
let wat = r#"
(module
(memory 1)
(func $leaf (export "leaf") (param i32) (result i64)
local.get 0
i64.load)
(func (export "caller") (param i32) (result i64)
local.get 0 call $leaf))
"#;
let report = compile_wcet_relocatable(wat, "cortex-m4");
assert_bounded(&report, "leaf", 54);
assert_bounded(&report, "caller", 84);
let f = func(&report, "caller");
let cycles = f.get("cycles").and_then(Value::as_u64).unwrap();
assert!(
cycles > 54,
"caller: composed bound {cycles} does not exceed the leaf's own 54 — \
composition did not actually add the callee in"
);
}
#[test]
fn i32_wrap_i64_after_priced_i64_ops_now_bounds() {
let wat = r#"
(module
(memory 1)
(func (export "narrow") (param i32) (result i32)
local.get 0 i64.load i64.const 3 i64.add i32.wrap_i64))
"#;
let report = compile_wcet_relocatable(wat, "cortex-m4");
assert_bounded(&report, "narrow", 80);
}
#[test]
fn i32_wrap_i64_relocatable_leaf_is_bounded() {
let wat = r#"
(module
(memory 1)
(func (export "w") (param i64) (result i32)
local.get 0
i32.wrap_i64))
"#;
let report = compile_wcet_relocatable(wat, "cortex-m4");
assert_bounded(&report, "w", 28);
}
#[test]
fn i64_extend_i32_s_relocatable_leaf_is_bounded() {
let wat = r#"
(module
(func (export "es") (param i32) (result i64)
local.get 0
i64.extend_i32_s))
"#;
let report = compile_wcet_relocatable(wat, "cortex-m4");
assert_bounded(&report, "es", 39);
}
#[test]
fn i64_extend_i32_u_relocatable_leaf_is_bounded() {
let wat = r#"
(module
(func (export "eu") (param i32) (result i64)
local.get 0
i64.extend_i32_u))
"#;
let report = compile_wcet_relocatable(wat, "cortex-m4");
assert_bounded(&report, "eu", 16);
}
#[test]
fn i64_extend_i32_s_store_composite_is_bounded() {
let wat = r#"
(module
(memory 1)
(func (export "ws") (param i32 i32)
local.get 0
local.get 1
i64.extend_i32_s
i64.store))
"#;
let report = compile_wcet_relocatable(wat, "cortex-m4");
assert_bounded(&report, "ws", 67);
}
#[test]
fn memory_size_still_declines_unmodeled_op() {
let wat = r#"
(module
(memory 1)
(func (export "ms") (result i32)
memory.size))
"#;
let report = compile_wcet_relocatable(wat, "cortex-m4");
let f = func(&report, "ms");
assert_eq!(
f.get("status").and_then(Value::as_str),
Some("declined"),
"ms: expected declined (MemorySize unpriced), got {f}"
);
assert_eq!(
f.get("reason").and_then(Value::as_str),
Some("unmodeled-op")
);
assert_eq!(
f.get("op").and_then(Value::as_str),
Some("MemorySize"),
"ms: the decline must NAME the op (#921); got {f}"
);
}
#[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");
}
const MASKED_REC_WAT: &str = r#"
(module
(func $md (export "md") (param i32) (result i32)
local.get 0 i32.const 15 i32.and
(if (result i32)
(then
local.get 0 i32.const 15 i32.and i32.const 1 i32.sub call $md i32.const 1 i32.add)
(else i32.const 0))))
"#;
#[test]
fn masked_recursion_correct_hint_converts_to_bound() {
let hints = r#"{"schema":"synth-wcet-hints-v1","functions":{"md":{"recursion_depth":15}}}"#;
let report = compile_wcet_hinted(MASKED_REC_WAT, "cortex-m4", Some(hints));
assert_bounded(&report, "md", 752);
let f = func(&report, "md");
let rec = f
.get("recursion")
.unwrap_or_else(|| panic!("bounded recursion must carry a `recursion` record: {f}"));
assert_eq!(
rec.get("max_depth").and_then(Value::as_u64),
Some(15),
"emitted depth must be synth's DERIVED ceiling (15), not the raw hint: {rec}"
);
assert_eq!(
rec.get("frame_count").and_then(Value::as_u64),
Some(16),
"frame_count must be max_depth+1 (the base frame counts): {rec}"
);
let instrs = f.get("instr_count").and_then(Value::as_u64).unwrap();
assert!(
752 >= 16 * instrs,
"bound 752 < 16 frames × {instrs} instrs — unsound"
);
}
#[test]
fn masked_recursion_unhinted_still_declines() {
let report = compile_wcet(MASKED_REC_WAT, "cortex-m4");
assert_declined(&report, "md", "recursion");
}
#[test]
fn masked_recursion_too_low_hint_rejected_red_first() {
let hints = r#"{"schema":"synth-wcet-hints-v1","functions":{"md":{"recursion_depth":3}}}"#;
let report = compile_wcet_hinted(MASKED_REC_WAT, "cortex-m4", Some(hints));
assert_declined(&report, "md", "recursion");
assert_hint_rejection(&report, "md", "hint-below-derived-depth", 3);
}
#[test]
fn tree_recursion_two_self_calls_rejected_even_with_hint() {
let wat = r#"
(module
(func $fib (export "fib") (param i32) (result i32)
local.get 0 i32.const 15 i32.and i32.const 2 i32.lt_s
(if (result i32)
(then i32.const 1)
(else
local.get 0 i32.const 15 i32.and i32.const 1 i32.sub call $fib
local.get 0 i32.const 15 i32.and i32.const 2 i32.sub call $fib
i32.add))))
"#;
let hints = r#"{"schema":"synth-wcet-hints-v1","functions":{"fib":{"recursion_depth":15}}}"#;
let report = compile_wcet_hinted(wat, "cortex-m4", Some(hints));
assert_declined(&report, "fib", "recursion");
assert_hint_rejection(&report, "fib", "hint-unverifiable-recursion", 15);
}
#[test]
fn uncapped_countdown_recursion_hint_rejected_unverifiable() {
let wat = r#"
(module
(func $count (export "count") (param i32) (result i32)
local.get 0 i32.eqz
(if (result i32)
(then i32.const 0)
(else local.get 0 i32.const 1 i32.sub call $count i32.const 1 i32.add))))
"#;
let hints = r#"{"schema":"synth-wcet-hints-v1","functions":{"count":{"recursion_depth":100}}}"#;
let report = compile_wcet_hinted(wat, "cortex-m4", Some(hints));
assert_declined(&report, "count", "recursion");
assert_hint_rejection(&report, "count", "hint-unverifiable-recursion", 100);
}
#[test]
fn mutual_recursion_stays_declined_even_with_hint() {
let wat = r#"
(module
(func $ping (export "ping") (param i32) (result i32)
local.get 0 i32.eqz
(if (result i32)
(then i32.const 0)
(else local.get 0 i32.const 1 i32.sub call $pong)))
(func $pong (export "pong") (param i32) (result i32)
local.get 0 i32.eqz
(if (result i32)
(then i32.const 1)
(else local.get 0 i32.const 1 i32.sub call $ping))))
"#;
let hints = r#"{"schema":"synth-wcet-hints-v1","functions":{"ping":{"recursion_depth":50}}}"#;
let report = compile_wcet_hinted(wat, "cortex-m4", Some(hints));
assert_declined(&report, "ping", "recursion");
assert_declined(&report, "pong", "recursion");
}
#[test]
fn conditional_decrement_recursion_rejected_unverifiable() {
let wat = r#"
(module
(func $f (export "f") (param i32) (result i32)
(local i32)
local.get 0 i32.const 15 i32.and
(if (result i32)
(then
(local.set 1 (i32.and (local.get 0) (i32.const 15)))
(if (i32.gt_s (local.get 0) (i32.const 100))
(then (local.set 1 (i32.sub (local.get 1) (i32.const 1)))))
(call $f (local.get 1))
i32.const 1 i32.add)
(else i32.const 0))))
"#;
let hints = r#"{"schema":"synth-wcet-hints-v1","functions":{"f":{"recursion_depth":15}}}"#;
let report = compile_wcet_hinted(wat, "cortex-m4", Some(hints));
assert_declined(&report, "f", "recursion");
assert_hint_rejection(&report, "f", "hint-unverifiable-recursion", 15);
}
const MASK_UP_WAT: &str = r#"
(module
(func (export "maskloop") (param i32) (result i32)
(local i32 i32)
(block
(loop
local.get 1 local.get 0 i32.const 7 i32.and i32.lt_s i32.eqz br_if 1
local.get 2 local.get 1 i32.add local.set 2
local.get 1 i32.const 1 i32.add local.set 1
br 0))
local.get 2))
"#;
const MASK_DOWN_WAT: &str = r#"
(module
(func (export "cd") (param i32) (result i32)
(local i32 i32)
(local.set 1 (i32.const 10))
(block
(loop
local.get 1 local.get 0 i32.const 7 i32.and i32.gt_s i32.eqz br_if 1
local.get 2 i32.const 1 i32.add local.set 2
local.get 1 i32.const 1 i32.sub local.set 1
br 0))
local.get 2))
"#;
#[test]
fn masked_ceiling_loop_unhinted_still_declines() {
let report = compile_wcet(MASK_UP_WAT, "cortex-m4");
assert_declined(&report, "maskloop", "loop");
}
#[test]
fn masked_ceiling_count_up_correct_hint_bounds() {
let hints = r#"{"schema":"synth-wcet-hints-v1","functions":{"maskloop":{"loop_bounds":[7]}}}"#;
let report = compile_wcet_hinted(MASK_UP_WAT, "cortex-m4", Some(hints));
assert_bounded(&report, "maskloop", 262);
assert_loop(&report, "maskloop", 0, 7, "mask-ceiling");
assert_trip_floor(&report, "maskloop");
}
#[test]
fn masked_ceiling_count_down_uses_both_endpoints() {
let hints = r#"{"schema":"synth-wcet-hints-v1","functions":{"cd":{"loop_bounds":[10]}}}"#;
let report = compile_wcet_hinted(MASK_DOWN_WAT, "cortex-m4", Some(hints));
assert_bounded(&report, "cd", 339);
assert_loop(&report, "cd", 0, 10, "mask-ceiling");
assert_trip_floor(&report, "cd");
}
#[test]
fn masked_ceiling_too_low_hint_rejected_red_first() {
let hints = r#"{"schema":"synth-wcet-hints-v1","functions":{"maskloop":{"loop_bounds":[3]}}}"#;
let report = compile_wcet_hinted(MASK_UP_WAT, "cortex-m4", Some(hints));
assert_declined(&report, "maskloop", "loop");
assert_hint_rejection(&report, "maskloop", "hint-below-derived-trip", 3);
}
#[test]
fn unmasked_data_dependent_loop_stays_declined_with_hint() {
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");
assert_hint_rejection(&report, "spin", "hint-unverifiable-induction", 100);
}
fn assert_hint_rejection(report: &Value, name: &str, reason: &str, hint: u64) {
let f = func(report, name);
let rej = f
.get("hint_rejections")
.and_then(Value::as_array)
.into_iter()
.flatten()
.find(|r| r.get("reason").and_then(Value::as_str) == Some(reason))
.unwrap_or_else(|| panic!("{name}: expected a hint rejection `{reason}` (entry: {f})"));
assert_eq!(
rej.get("hint").and_then(Value::as_u64),
Some(hint),
"{name}: rejection carries the offered hint value (record: {rej})"
);
}
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"
);
}
}
const NAMED_INTERNAL_WAT: &str = r#"
(module
(func $_RNvCs942N1ctoMYm_4fixt12inner_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)
(func (export "entry") (result i32)
call $_RNvCs942N1ctoMYm_4fixt12inner_eqexit))
"#;
const RAW_NAME: &str = "_RNvCs942N1ctoMYm_4fixt12inner_eqexit";
const STABLE_KEY: &str = "_RNvC4fixt12inner_eqexit";
fn compile_wcet_capture(
wat: &str,
triple: &str,
hints_json: Option<&str>,
) -> (Value, String, Vec<u8>) {
let dir = std::env::temp_dir().join(format!(
"synth_wcet_key_{}_{}_{}",
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 out = Command::new(synth())
.args(&args)
.output()
.expect("failed to run synth compile");
assert!(out.status.success(), "synth compile failed for {triple}");
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
let elf = std::fs::read(&out_path).unwrap();
let sidecar = {
let mut s = out_path.into_os_string();
s.push(".wcet.json");
std::path::PathBuf::from(s)
};
let report =
serde_json::from_str(&std::fs::read_to_string(&sidecar).unwrap()).expect("sidecar JSON");
(report, stderr, elf)
}
#[test]
fn name_section_identity_replaces_func_index_and_emits_key_contract() {
let (report, _, _) = compile_wcet_capture(NAMED_INTERNAL_WAT, "cortex-m4", None);
assert_declined(&report, RAW_NAME, "loop");
let f = func(&report, RAW_NAME);
let hk = f
.get("hint_key")
.unwrap_or_else(|| panic!("entry must emit the hint_key contract: {f}"));
assert_eq!(hk.get("key").and_then(Value::as_str), Some(STABLE_KEY));
assert_eq!(
hk.get("build_local"),
None,
"the stripped key is stable — must not be flagged build-local: {hk}"
);
let e = func(&report, "entry");
assert_eq!(
e.get("hint_key")
.and_then(|k| k.get("key"))
.and_then(Value::as_str),
Some("entry")
);
}
#[test]
fn hint_keyed_on_name_section_name_converts_loop_decline() {
let hints = format!(
r#"{{"schema":"synth-wcet-hints-v1","functions":{{"{RAW_NAME}":{{"loop_bounds":[8]}}}}}}"#
);
let (report, stderr, _) = compile_wcet_capture(NAMED_INTERNAL_WAT, "cortex-m4", Some(&hints));
assert_loop(&report, RAW_NAME, 0, 8, "hint-verified");
assert_trip_floor(&report, RAW_NAME);
assert_eq!(
func(&report, "entry").get("status").and_then(Value::as_str),
Some("bounded")
);
assert!(
!stderr.contains("not consumed"),
"a matching hint must not warn: {stderr}"
);
}
#[test]
fn stable_key_accepted_and_emitted_trip_is_derived_never_raw_hint() {
let hints = format!(
r#"{{"schema":"synth-wcet-hints-v1","functions":{{"{STABLE_KEY}":{{"loop_bounds":[100]}}}}}}"#
);
let (report, _, _) = compile_wcet_capture(NAMED_INTERNAL_WAT, "cortex-m4", Some(&hints));
let f = func(&report, RAW_NAME);
let l = &f.get("loops").and_then(Value::as_array).unwrap()[0];
assert_eq!(
l.get("trip_count").and_then(Value::as_u64),
Some(8),
"emitted trip must be synth's DERIVED ceiling, never the raw hint: {l}"
);
assert_eq!(l.get("hint").and_then(Value::as_u64), Some(100));
}
#[test]
fn wrong_hint_via_name_section_key_still_rejected_below_derived() {
let hints = format!(
r#"{{"schema":"synth-wcet-hints-v1","functions":{{"{RAW_NAME}":{{"loop_bounds":[3]}}}}}}"#
);
let (report, _, _) = compile_wcet_capture(NAMED_INTERNAL_WAT, "cortex-m4", Some(&hints));
assert_declined(&report, RAW_NAME, "loop");
let rej = &func(&report, RAW_NAME)
.get("hint_rejections")
.and_then(Value::as_array)
.unwrap()[0];
assert_eq!(
rej.get("reason").and_then(Value::as_str),
Some("hint-below-derived-trip")
);
}
#[test]
fn index_key_for_named_function_is_refused_with_named_reason() {
let hints = r#"{"schema":"synth-wcet-hints-v1","functions":{"func_0":{"loop_bounds":[8]}}}"#;
let (report, stderr, _) = compile_wcet_capture(NAMED_INTERNAL_WAT, "cortex-m4", Some(hints));
assert_declined(&report, RAW_NAME, "loop");
assert!(
stderr.contains("wcet-hint-key-index-refused"),
"the refusal must be NAMED on stderr: {stderr}"
);
assert!(
stderr.contains(STABLE_KEY),
"the refusal must state the key to use instead: {stderr}"
);
let d = &report["hints"]["diagnostics"][0];
assert_eq!(
d.get("reason").and_then(Value::as_str),
Some("wcet-hint-key-index-refused"),
"the refusal must be NAMED in the sidecar: {report}"
);
}
#[test]
fn nameless_internal_keeps_func_index_build_local_and_unknown_key_warns() {
const NAMELESS_WAT: &str = r#"
(module
(func (result i32) i32.const 7)
(func (export "entry") (result i32) call 0))
"#;
let hints = r#"{"schema":"synth-wcet-hints-v1","functions":{"nosuch":{"loop_bounds":[8]}}}"#;
let (report, stderr, _) = compile_wcet_capture(NAMELESS_WAT, "cortex-m4", Some(hints));
let f = func(&report, "func_0");
let hk = f.get("hint_key").expect("func_0 must carry the contract");
assert_eq!(hk.get("key").and_then(Value::as_str), Some("func_0"));
assert_eq!(
hk.get("build_local").and_then(Value::as_bool),
Some(true),
"an index is not an identity — it must be flagged build-local: {hk}"
);
assert!(
stderr.contains("nosuch") && stderr.contains("not in this module"),
"an unknown key must warn loudly: {stderr}"
);
}
#[test]
fn name_keys_and_hints_are_byte_invisible_in_the_elf() {
let hints = format!(
r#"{{"schema":"synth-wcet-hints-v1","functions":{{"{RAW_NAME}":{{"loop_bounds":[8]}}}}}}"#
);
let (_, _, elf_unhinted) = compile_wcet_capture(NAMED_INTERNAL_WAT, "cortex-m4", None);
let (_, _, elf_hinted) = compile_wcet_capture(NAMED_INTERNAL_WAT, "cortex-m4", Some(&hints));
assert_eq!(
elf_unhinted, elf_hinted,
"--wcet-hints / #1063 identities must never move a byte of the object"
);
}
#[test]
fn sidecar_discriminates_no_hints_consumed_and_all_refused() {
let (report_a, _, _) = compile_wcet_capture(NAMED_INTERNAL_WAT, "cortex-m4", None);
assert!(
report_a.get("hints").is_none(),
"no --wcet-hints => no top-level `hints` object: {report_a}"
);
let hints_ok = format!(
r#"{{"schema":"synth-wcet-hints-v1","functions":{{"{RAW_NAME}":{{"loop_bounds":[8]}}}}}}"#
);
let (report_b, _, _) = compile_wcet_capture(NAMED_INTERNAL_WAT, "cortex-m4", Some(&hints_ok));
let h = report_b
.get("hints")
.unwrap_or_else(|| panic!("hints supplied => `hints` object required: {report_b}"));
let resolved = h.get("resolved").and_then(Value::as_array).unwrap();
assert_eq!(resolved.len(), 1, "one hint resolved: {h}");
assert_eq!(
resolved[0].get("key").and_then(Value::as_str),
Some(RAW_NAME)
);
assert_eq!(
resolved[0].get("function").and_then(Value::as_str),
Some(RAW_NAME),
"resolved entry must name the function by its sidecar display name: {h}"
);
assert_eq!(
h.get("diagnostics").and_then(Value::as_array).map(Vec::len),
Some(0),
"a cleanly consumed hint produces no diagnostics: {h}"
);
let hints_bad =
r#"{"schema":"synth-wcet-hints-v1","functions":{"func_0":{"loop_bounds":[8]}}}"#;
let (report_c, stderr_c, _) =
compile_wcet_capture(NAMED_INTERNAL_WAT, "cortex-m4", Some(hints_bad));
let h = report_c
.get("hints")
.unwrap_or_else(|| panic!("all-refused hints must still emit `hints`: {report_c}"));
assert_eq!(
h.get("resolved").and_then(Value::as_array).map(Vec::len),
Some(0),
"nothing resolved: {h}"
);
let diags = h.get("diagnostics").and_then(Value::as_array).unwrap();
assert_eq!(diags.len(), 1, "one refused entry => one diagnostic: {h}");
let d = &diags[0];
assert_eq!(d.get("key").and_then(Value::as_str), Some("func_0"));
assert_eq!(
d.get("reason").and_then(Value::as_str),
Some("wcet-hint-key-index-refused"),
"the sidecar must carry the SAME machine tag stderr names: {d}"
);
assert_eq!(
d.get("function").and_then(Value::as_str),
Some(RAW_NAME),
"index-refused DOES resolve to a known function — name it: {d}"
);
let detail = d.get("detail").and_then(Value::as_str).unwrap();
assert!(
detail.contains(STABLE_KEY),
"the detail must state the key to use instead: {d}"
);
assert!(
stderr_c.contains("wcet-hint-key-index-refused"),
"{stderr_c}"
);
}
#[test]
fn sidecar_carries_unknown_key_diagnostic_without_function() {
let hints = r#"{"schema":"synth-wcet-hints-v1","functions":{"nosuch":{"loop_bounds":[8]}}}"#;
let (report, _, _) = compile_wcet_capture(NAMED_INTERNAL_WAT, "cortex-m4", Some(hints));
let h = report
.get("hints")
.expect("hints supplied => object present");
let diags = h.get("diagnostics").and_then(Value::as_array).unwrap();
assert_eq!(diags.len(), 1, "{h}");
assert_eq!(
diags[0].get("reason").and_then(Value::as_str),
Some("wcet-hint-key-unknown")
);
assert_eq!(diags[0].get("key").and_then(Value::as_str), Some("nosuch"));
assert!(
diags[0].get("function").is_none(),
"an unknown key resolves to NO function — the field must be absent: {}",
diags[0]
);
}
#[test]
fn refused_hint_is_byte_invisible_in_the_elf() {
let hints_bad =
r#"{"schema":"synth-wcet-hints-v1","functions":{"func_0":{"loop_bounds":[8]}}}"#;
let (_, _, elf_unhinted) = compile_wcet_capture(NAMED_INTERNAL_WAT, "cortex-m4", None);
let (_, _, elf_refused) =
compile_wcet_capture(NAMED_INTERNAL_WAT, "cortex-m4", Some(hints_bad));
assert_eq!(
elf_unhinted, elf_refused,
"a refused --wcet-hints entry must never move a byte of the object"
);
}