Skip to content

feat(gust): WCET EVT cross-check on real F100 — static bounds dominate (v0.6.0 D1) - #229

Merged
avrabe merged 2 commits into
mainfrom
feat/wcet-evt-harness
Jul 29, 2026
Merged

feat(gust): WCET EVT cross-check on real F100 — static bounds dominate (v0.6.0 D1)#229
avrabe merged 2 commits into
mainfrom
feat/wcet-evt-harness

Conversation

@avrabe

@avrabe avrabe commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

v0.6.0 workstream D1. Track T4 had an implemented requirement but an open V — the compiler emits sound static bounds, and nothing checked them against reality. This closes the measurement half, on real silicon.

Measured on a physical STM32VLDISCOVERY (DWT cycle counter, n=40 each)

function static bound (synth --emit-wcet) observed max margin
gust:os/time@0.1.0#deadline 42 40 (min 34, mean 37) +2
gust:os/time@0.1.0#elapsed 71 45 (min 39, mean 42) +26

The static bound dominates every observed sample and the EVT statistical bound, for both functionscheck-wcet-evt.pyGATE GREEN. Inputs span the u64 wrap boundary (0, 1, u64::MAX, 2^63, 2^63−1, mixed magnitudes). Overhead calibrated: read_read=1 cyc subtracted; the caller's bl/bx shim (5 cyc) deliberately not subtracted, so samples stay conservative.

(Pleasing convergence: these are the same wrap-safe deadline/elapsed I Kani-proved in #222 — the function is now both formally proven and timing-bounded-and-measured.)

What this does NOT claim

It records VER-OS-WCET-EVT-001 and leaves VER-OS-WCET-001 proposed. That artifact's acceptance is stated over every function of the trusted switch/MPU/HM object and the inner executor core — those still have no static bound at all (synth declines them loudly: reason=call needs T3/spar composition, reason=loop needs trip-count inference). Narrowing the artifact to match what I achieved would be moving the goalposts, so the gap is stated instead.

Worst-path honesty (the subtle part)

Both functions open with a once-init guard whose byte is non-zero in the frozen .data image — so a stock call never executes the store block the static bound covers (11 of 18 instructions run). Sampling only that would leave the bounded path unmeasured. The harness therefore zeroes that byte for half the samples, proves the decode at runtime before relying on it (path_variants=cold+warm), and restores it.

Noted, not gated: deadline's naive tail projection (max(block)+dispersion = 46 cyc) sits above its 42 cyc bound. The kill-criterion (observed or statistical > static) is not met, so the gate is green — but the margin is thin and the checker prints this rather than hiding it. No knob exists to turn that green; a human adjudicates.

Mechanized kill-criterion

benches/gust/drivers/check-wcet-evt.py reads bounds from the committed sidecar (never hardcoded), refuses to gate a declined entry rather than inventing a bound, exits 1 when an observed/statistical value exceeds the static bound, and proves its own red state via --self-test. Capture committed at silicon/RESULTS-wcet-evt-f100.txt. rivet validate PASS.

🤖 Generated with Claude Code

avrabe and others added 2 commits July 28, 2026 21:49
…nds (v0.6.0 D1)

VER-OS-WCET-001 requires synth's static per-function cycle bound to DOMINATE both
a measurement-based statistical (EVT) bound and every observed raw DWT sample, and
its kill-criterion is the negation of that. This lands the measurement half plus the
kill-criterion as a mechanical gate, for the two functions of the frozen os-tl node
that actually carry a static bound (`status: bounded` in repro-757/os-tl.wcet.json).

- benches/gust/src/bin/gust_wcet_evt.rs — F100 firmware DWT-sampling the DISSOLVED
  `gust:os/time@0.1.0#deadline` (42 cyc) and `#elapsed` (71 cyc) out of the frozen
  os-tl-fixed.o. N=40 per function over a wrap-boundary-heavy input set, integer
  stats only, one machine-parseable line per sample plus a per-function summary.
  Three things make the numbers mean something:
   * a correctness gate runs FIRST (deadline==now+ticks, elapsed==now>=deadline over
     all 20 pairs) — no timings are reported for code that computes the wrong thing;
   * both functions open with a once-init guard whose byte
     (__synth_wasm_seg_2+12) is 0x3a in the frozen .data image, so the store path the
     bound covers (instr_count 18 vs 11) is never taken on a stock run. The harness
     zeroes that byte for the `cold` half of the samples to exercise it, and PROVES
     the decode at runtime before relying on it (byte reads 0x3a; reads 0x01 after a
     zeroed call), degrading loudly to warm-only if the proof fails;
   * the DWT is liveness-checked and the run REFUSES to emit timings where the cycle
     counter is absent (qemu) rather than reporting emulated zeros.
  Measurement overhead is calibrated two ways: `read_read` (two back-to-back CYCCNT
  reads, SUBTRACTED) and `call_shim` (bl + bx lr, reported but NOT subtracted, so
  samples stay conservative). The linked disassembly was checked: the only thing
  between the two CYCCNT reads is the `bl` itself.

- benches/gust/drivers/check-wcet-evt.py — the kill-criterion as a build gate.
  Reads the bounds from the committed synth-wcet-v1 sidecar (never hardcoded),
  matches by WIT-export suffix, computes a block-maxima EVT statistic over the raw
  samples, and exits 1 if an observed max OR the statistical bound exceeds the
  static bound. Refuses to gate a `declined` sidecar entry, and fails on a truncated
  capture, n<30, or a capture whose summary disagrees with its samples. `--self-test`
  demonstrates the red state without hardware; `--emit-sample-capture {pass,fail}`
  produces sidecar-derived fixtures. Output is explicit that for these straight-line
  functions the sample distribution is degenerate, so the EVT bound is tight by
  construction and this is a CROSS-CHECK, not an independent bound — and that DWT
  numbers may only falsify the static model, never size a partition budget.

- build.rs links the object into the new bin behind the synth#746-class
  defined-text-symbol guard; Cargo.toml gates the bin on target-f100 so the default
  g474/thumbv7em build skips it (a cortex-m3 .o in a v7E-M image links to nothing).

Oracles (all run, exact output in the PR): f100 build clean with 0 undefined symbols
in the ELF and both `bl`s landing on the right exports; --self-test exits 1;
synthetic passing capture exits 0; default g474 --bins build exits 0 and skips the
bin. Pre-silicon smoke under qemu passes the correctness gate and the worst-path
decode proof, then correctly refuses to time (no DWT).

NOT YET DONE: no silicon numbers. The coordinator owns the board; nothing here
assumes or asserts a measured value.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e (v0.6.0 D1)

Closes the MEASUREMENT half of track T4 for the functions that actually carry a
static bound, and records it as VER-OS-WCET-EVT-001 rather than closing
VER-OS-WCET-001 — whose acceptance is stated over the trusted switch/MPU/HM object
and the inner executor core, which still have NO static bound (synth declines them:
reason=call needs T3 composition, reason=loop needs trip-count inference). Narrowing
that artifact to match what was achieved would be moving the goalposts; it stays
proposed with the gap stated.

MEASURED on real STM32VLDISCOVERY silicon (openocd/ST-LINK-V1 via the Pi), DWT
cycle counter, n=40 per function, inputs spanning the u64 wrap boundary:
  deadline  min=34 max=40 mean=37  vs static  42  -> dominates, margin  2 cyc
  elapsed   min=39 max=45 mean=42  vs static  71  -> dominates, margin 26 cyc
Overhead calibrated (read_read=1 cyc subtracted; the caller's bl/bx shim 5 cyc
deliberately NOT subtracted, so samples stay conservative).

Worst-path honesty: both functions open with a once-init guard whose byte is
non-zero in the frozen .data, so a stock call never executes the store block the
bound covers. The harness zeroes it for half the samples and PROVES the decode at
runtime first (path_variants=cold+warm) — measuring only the default path would
have left the bounded path unmeasured. Noted-not-gated: deadline's naive tail
projection (46 cyc) sits above its 42 cyc bound; the margin is thin and the checker
prints that rather than hiding it.

check-wcet-evt.py mechanizes the kill-criterion (exit 1 when observed or EVT bound
exceeds the static bound), reads bounds from the committed sidecar, refuses to gate
declined entries, and proves its own red state via --self-test. GATE GREEN on the
committed capture. rivet validate PASS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@avrabe
avrabe merged commit b60b6ed into main Jul 29, 2026
61 checks passed
@avrabe
avrabe deleted the feat/wcet-evt-harness branch July 29, 2026 03:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant