synth_core/wcet.rs
1//! #778 (v0.46 Wave-1 Lane 2) — the `synth-wcet-v1` static worst-case-cycle map.
2//!
3//! synth holds the EXACT final instruction sequence of every compiled function,
4//! so it is the natural owner of a SOUND static per-function worst-case execution
5//! time (WCET) bound. gale's schedulability track (spar T3/T4) computes a
6//! machine-checked response-time bound, but its per-task cost inputs (`C_i`) are
7//! only DWT high-water-marks — *observations*, not *bounds* — and a hard build
8//! gate forbids sizing budgets from DWT. This sidecar supplies the missing SOUND
9//! input: a cycle bound that is provably ≥ any real execution of the function.
10//!
11//! ## Soundness contract (the whole point)
12//!
13//! A bound that is EVER less than the real cycle count is a defect. This module
14//! is therefore deliberately conservative and DECLINES loudly rather than emit a
15//! number it cannot defend:
16//!
17//! - **Loop-free functions** get an EXACT-form bound: every instruction in the
18//! final stream executes at most once, so the bound is the SUM of each
19//! instruction's documented worst-case cycles. Summing every instruction
20//! (including both arms of an `if/else`) is an over-estimate, hence sound; no
21//! path enumeration is needed.
22//! - **Loops with statically-evident trip counts** (#778 phase 2): a canonical
23//! counted loop — const-initialized counter, const step, const bound, single
24//! backward branch — whose trip count synth PROVES from the final instruction
25//! stream gets `trip × body-worst + overhead` as an upper bound; every
26//! instruction's cost is multiplied by its proven worst-case execution count.
27//! Nested loops multiply only when EVERY level proves.
28//! - **Everything else** — any loop synth cannot prove a trip count for
29//! (data-dependent bounds, non-canonical shapes), any residual/external
30//! label branch (unknown direction), any call (`Bl`/`Blx`, inter-procedural),
31//! any op whose encoder expansion contains an internal runtime loop
32//! (`i64` software div/rem), any unsupported core class — is DECLINED with a
33//! machine-readable reason. gale cannot size a budget from an unsound number,
34//! so a decline is strictly better than a guess.
35//!
36//! `--wcet-hints` (#778 phase 2, the scry seam) supplies UNTRUSTED per-loop
37//! trip-count hints; each is soundly CHECKED against synth's own induction
38//! proof before use and REJECTED with a machine reason otherwise (see
39//! [`WcetHints`] / [`WcetHintReject`]). Richer hint certificates (data-dependent
40//! bounds) and inter-procedural composition remain the named scry / spar
41//! follow-ups, explicitly OUT of scope.
42//!
43//! ## Precondition — a bound without its assumptions is not a safety input
44//!
45//! The per-instruction cycle numbers are documented worst cases for the
46//! **Cortex-M3 / Cortex-M4(F)** in-order pipeline under a **zero-wait-state**
47//! instruction memory (flash accelerator / I-cache hit). The bound is CONDITIONAL
48//! on that precondition, which is recorded in the JSON (`core_class`,
49//! `wait_states`, `memory_assumption`) so the T4 consumer knows exactly what it
50//! holds under. Cortex-M7 (dual-issue + caches with wait-states that can make
51//! actual cycles EXCEED a zero-wait straight sum) is DECLINED, not
52//! approximated — soundness over coverage.
53//!
54//! ## Schema (`synth-wcet-v1`)
55//!
56//! A JSON sidecar written next to the object (`<output>.wcet.json`). Purely
57//! additive metadata: it is derived from the already-decided instruction stream
58//! and never touches `.text`, so the emitted bytes are byte-identical whether or
59//! not the bound is emitted (frozen-safe).
60
61use serde::{Deserialize, Serialize};
62
63/// The schema version string embedded at the top of the sidecar.
64pub const SCHEMA: &str = "synth-wcet-v1";
65
66/// The schema string a `--wcet-hints` file must carry (#778 phase 2).
67pub const HINTS_SCHEMA: &str = "synth-wcet-hints-v1";
68
69/// Why a function could not receive a sound static cycle bound. Each variant is a
70/// distinct, machine-readable decline reason so a consumer (spar T4) can tell an
71/// unbounded loop from an inter-procedural edge from an unsupported core.
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "kebab-case")]
74pub enum WcetDecline {
75 /// A backward branch in the final instruction stream — a loop — whose trip
76 /// count synth could NOT statically prove (#778 phase 2 proves canonical
77 /// const-init/const-step/const-bound counted loops; equality-exit shapes
78 /// additionally need a verified `--wcet-hints` entry). Data-dependent
79 /// bounds remain the scry loop-bound-inference follow-up.
80 Loop,
81 /// A DIRECT call (`Bl func_N`) that could not be composed into a bound because
82 /// the callee is unbounded or unresolvable in THIS module — an external/imported
83 /// callee (a WASM import, `__meld_dispatch_import`, or an `__aeabi_*` runtime
84 /// helper: it has no per-function body in this module to sum). #778 phase 3
85 /// composes direct calls to LOCAL, bounded callees over the direct call graph;
86 /// this reason remains for the direct edges that cannot be composed.
87 Call,
88 /// A cycle in the direct call graph (self-recursion or mutual recursion). An
89 /// upper cycle bound cannot be composed from a call graph that revisits a frame
90 /// an unbounded number of times, so every function on the cycle DECLINES. This
91 /// is the #778 phase-3 decline-honesty guard: composition only bounds an acyclic
92 /// direct call graph.
93 Recursion,
94 /// An INDIRECT call (`Blx <reg>` / `call_indirect` / a function-pointer
95 /// dispatch such as `__meld_dispatch_import`): the callee is not statically
96 /// known, so its bound cannot be composed. Declined, not guessed. (#778 phase 3.)
97 IndirectCall,
98 /// A caller whose own body is bounded but that DIRECTLY calls a callee which
99 /// itself declined (transitively): a decline must PROPAGATE up the call graph —
100 /// a caller cannot be bounded while a callee it invokes is unbounded. (#778
101 /// phase 3.) The `note()` names the first unbounded callee for diagnosis.
102 CalleeUnbounded,
103 /// A residual/external label branch (`B`/`Bcc`/… still carrying a label): its
104 /// direction is not statically known here, so it cannot be proven loop-free.
105 UnresolvedBranch,
106 /// An op whose encoder expansion contains an internal RUNTIME loop (the `i64`
107 /// software div/rem shift-subtract: emitted once but executed 64×). Its body
108 /// bytes appear once in the stream, so a straight sum would undercount — a
109 /// sound bound needs a per-op `trip × body` model, a named follow-up.
110 LoopedExpansion,
111 /// The target core class is not soundly summable with a zero-wait per-op table
112 /// (Cortex-M7/M7dp: dual-issue + cache wait-states). Declined, not
113 /// approximated.
114 UnsupportedCore,
115 /// An op the cycle model has not classified. Never emitted in a released
116 /// build (the classifier is exhaustive with no wildcard) — present so the
117 /// schema can carry a conservative decline if the table is ever incomplete.
118 UnmodeledOp,
119}
120
121impl WcetDecline {
122 /// A short human-readable explanation, embedded alongside the machine reason.
123 pub fn note(&self) -> &'static str {
124 match self {
125 WcetDecline::Loop => {
126 "backward branch (loop) without a statically-proven trip count — \
127 canonical const-bound counted loops are proven automatically; \
128 equality-exit shapes need a verified --wcet-hints entry; \
129 data-dependent bounds are the scry loop-bound-inference follow-up"
130 }
131 WcetDecline::Call => {
132 "direct call to an external/imported/unresolvable callee with no \
133 per-function bound in this module — cannot compose an \
134 inter-procedural bound (local direct calls ARE composed, #778 phase 3)"
135 }
136 WcetDecline::Recursion => {
137 "cycle in the direct call graph (self- or mutual recursion) — an \
138 upper cycle bound cannot be composed from a recursive call graph"
139 }
140 WcetDecline::IndirectCall => {
141 "indirect call (Blx <reg> / call_indirect / function-pointer \
142 dispatch) — the callee is not statically known, cannot compose"
143 }
144 WcetDecline::CalleeUnbounded => {
145 "a directly-called callee is itself unbounded — the decline \
146 propagates up the call graph (a caller cannot be bounded while a \
147 callee it invokes is unbounded)"
148 }
149 WcetDecline::UnresolvedBranch => {
150 "residual external/unresolved label branch — direction not \
151 statically known, cannot prove loop-free"
152 }
153 WcetDecline::LoopedExpansion => {
154 "op expands to an internal runtime loop (i64 software div/rem, \
155 executed 64×) — straight sum would undercount"
156 }
157 WcetDecline::UnsupportedCore => {
158 "core class not soundly summable with a zero-wait per-op table \
159 (Cortex-M7 dual-issue + cache wait-states)"
160 }
161 WcetDecline::UnmodeledOp => "op not classified by the cycle model",
162 }
163 }
164}
165
166/// How a loop's trip count was established (#778 phase 2).
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(rename_all = "kebab-case")]
169pub enum WcetLoopBoundSource {
170 /// Fully static proof: const-initialized counter, const step, const bound,
171 /// exit-guaranteeing comparison — the trip count is derived by synth alone.
172 Static,
173 /// The loop is an equality-exit shape synth only bounds under an explicit
174 /// `--wcet-hints` assertion; the hint was CHECKED against synth's own derived
175 /// trip count (divisibility + monotonicity + derived ≤ hint) before use. The
176 /// emitted trip count is still synth's DERIVED value, never the raw hint.
177 HintVerified,
178 /// (#778 phase 5) The loop's exit bound is a DATA-DEPENDENT masked ceiling
179 /// (`i REL (x & K)` for a runtime `x`): the real per-iteration bound lies in
180 /// `[0, K]` for ANY input (`x & K ∈ [0,K]`), so synth DERIVES the worst-case
181 /// trip as the MAX over both endpoints of that interval (`rhs = K` and
182 /// `rhs = 0`, both required to terminate) — an entry-independent ceiling.
183 /// Like [`HintVerified`] this is HINT-GATED: the derived trip is consumed
184 /// only under an explicit `--wcet-hints` entry the derived count respects
185 /// (`derived ≤ hint`); the emitted trip is synth's DERIVED value, never the
186 /// raw hint. A distinct source (not `HintVerified`) so the sidecar states the
187 /// extra data-dependent-ceiling assumption the bound rests on.
188 MaskCeiling,
189}
190
191/// One proven-bounded loop inside a bounded function (#778 phase 2). Loops are
192/// listed in ascending `head_offset` order — the SAME order `--wcet-hints`
193/// `loop_bounds` entries are matched by.
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct WcetLoopBound {
196 /// Byte offset of the loop head (backward-branch target) within the function.
197 pub head_offset: u64,
198 /// The PROVEN maximum number of body executions (full iterations).
199 pub trip_count: u64,
200 /// Number of instructions inside the loop region (head..=backward branch),
201 /// so a consumer can cross-check `cycles ≥ trip_count × region_instr_count`
202 /// (every instruction costs ≥ 1 cycle).
203 pub region_instr_count: usize,
204 /// How the trip count was established.
205 pub source: WcetLoopBoundSource,
206 /// The hint value consumed (present iff `source == HintVerified` or a
207 /// redundant hint was cross-checked against a static proof).
208 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub hint: Option<u64>,
210}
211
212/// Machine-readable reason a `--wcet-hints` entry was REJECTED (#778 phase 2).
213/// The hint file is UNTRUSTED input: a hint is only ever consumed after synth
214/// verifies the loop's induction against it; everything else lands here.
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(rename_all = "kebab-case")]
217pub enum WcetHintReject {
218 /// The hint is SMALLER than synth's own derived trip count — a wrong hint.
219 /// Trusting it would emit a bound < a real execution (the fatal class).
220 HintBelowDerivedTrip,
221 /// synth could not verify the loop's induction against the hint (counter not
222 /// provably monotonic toward a statically-known bound ≤ hint — e.g. a
223 /// data-dependent bound register, a non-canonical shape, or an equality exit
224 /// whose step does not divide the distance). An unverifiable hint is never
225 /// trusted into a bound.
226 HintUnverifiableInduction,
227 /// The hint indexes a loop that does not exist in this function's final
228 /// instruction stream.
229 HintUnknownLoop,
230 /// A recursion-depth hint (`recursion_depth`) was offered but synth could NOT
231 /// verify the self-recursion is a single-self-call chain whose controlling
232 /// value is entry-independently bounded (a masked-slot counter decreasing by a
233 /// const step toward a base guard on the SAME masked quantity). Without an
234 /// entry-independent ceiling the true depth is runtime-unbounded, so the hint
235 /// is never trusted into a bound. (#778 phase 4 / #49.)
236 HintUnverifiableRecursion,
237 /// A recursion-depth hint is SMALLER than synth's own DERIVED maximum depth
238 /// (the entry-independent ceiling proven from the masked-slot induction). A
239 /// hint below the derived depth is a wrong oracle claim — trusting it would
240 /// emit a bound < a real execution (the fatal class). (#778 phase 4 / #49.)
241 HintBelowDerivedDepth,
242}
243
244impl WcetHintReject {
245 /// A short human-readable explanation, embedded alongside the machine reason.
246 pub fn note(&self) -> &'static str {
247 match self {
248 WcetHintReject::HintBelowDerivedTrip => {
249 "hint is below synth's derived trip count — a wrong hint; \
250 trusting it would emit an unsound bound"
251 }
252 WcetHintReject::HintUnverifiableInduction => {
253 "loop induction not verifiable against the hint (counter not \
254 provably monotonic toward a statically-known bound ≤ hint) — \
255 an unverifiable hint is never trusted into a bound"
256 }
257 WcetHintReject::HintUnknownLoop => {
258 "hint indexes a loop that does not exist in the final \
259 instruction stream"
260 }
261 WcetHintReject::HintUnverifiableRecursion => {
262 "recursion-depth hint not verifiable — the self-recursion is not a \
263 single-self-call chain whose controlling value is entry-independently \
264 bounded (masked-slot counter decreasing by a const step toward a base \
265 guard on the same masked quantity); depth is runtime-unbounded, so \
266 the hint is never trusted into a bound"
267 }
268 WcetHintReject::HintBelowDerivedDepth => {
269 "recursion-depth hint is below synth's derived maximum depth (the \
270 entry-independent ceiling proven from the masked-slot induction) — \
271 a wrong hint; trusting it would emit an unsound bound"
272 }
273 }
274 }
275}
276
277/// One rejected hint, recorded in the sidecar so the oracle (scry) sees exactly
278/// which of its claims synth refused and why.
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280pub struct WcetHintRejection {
281 /// Index into the function's `loop_bounds` hint array (== loop order by
282 /// ascending head offset).
283 pub loop_index: usize,
284 /// Byte offset of the loop head this hint addressed, when the loop exists.
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub head_offset: Option<u64>,
287 /// The rejected hint value.
288 pub hint: u64,
289 /// Machine-readable rejection reason.
290 pub reason: WcetHintReject,
291 /// Human-readable note (`reason.note()`).
292 pub note: String,
293}
294
295/// (#778 phase 4 / #49) The self-recursion record carried on a bounded function
296/// whose bound was composed via a verified recursion-depth certificate, so the
297/// sidecar states exactly how the frame count was established (and that a hint gated
298/// it — the derived depth is still synth's own).
299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
300pub struct WcetRecursionBound {
301 /// The DERIVED maximum recursion depth (entry-independent ceiling).
302 pub max_depth: u64,
303 /// The number of frames folded into the bound (`max_depth + 1`, counting the
304 /// base frame). Diagnostic — lets a consumer cross-check `cycles ≥ frames`.
305 pub frame_count: u64,
306 /// The `--wcet-hints` `recursion_depth` value that gated the certificate (the
307 /// emitted `max_depth` is synth's DERIVED value, never this raw hint).
308 pub hint: u64,
309}
310
311/// The per-function result: either a sound cycle bound or a loud decline.
312#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
313#[serde(tag = "status", rename_all = "kebab-case")]
314pub enum WcetFunction {
315 /// A sound upper bound on this function's execution in cycles.
316 Bounded {
317 /// Function name (WASM export or generated).
318 name: String,
319 /// The sound worst-case cycle bound. For a loop-free function this is the
320 /// SUM of each instruction's documented worst-case cycles (each executes
321 /// at most once). For a function whose loops ALL have proven trip counts
322 /// (#778 phase 2) each instruction's cost is multiplied by its proven
323 /// worst-case execution count. Always ≥ any real execution under the
324 /// stated precondition.
325 cycles: u64,
326 /// Number of ARM instructions summed (diagnostic).
327 instr_count: usize,
328 /// Proven loops (empty for a loop-free function), ascending head offset.
329 #[serde(default, skip_serializing_if = "Vec::is_empty")]
330 loops: Vec<WcetLoopBound>,
331 /// (#778 phase 4 / #49) Present iff this bound was composed via a verified
332 /// self-recursion certificate; states the derived depth + frame count.
333 #[serde(default, skip_serializing_if = "Option::is_none")]
334 recursion: Option<WcetRecursionBound>,
335 /// Hints that were rejected (the static proof stands independently).
336 #[serde(default, skip_serializing_if = "Vec::is_empty")]
337 hint_rejections: Vec<WcetHintRejection>,
338 },
339 /// No bound emitted — a loud decline with a machine-readable reason. A decline
340 /// is emitted (rather than the function omitted) so the map is COMPLETE: a
341 /// consumer sees every function is either bounded or explicitly unbounded,
342 /// never silently missing.
343 Declined {
344 /// Function name.
345 name: String,
346 /// Machine-readable reason.
347 reason: WcetDecline,
348 /// Human-readable note (`reason.note()`).
349 note: String,
350 /// Hints that were offered for this function and rejected.
351 #[serde(default, skip_serializing_if = "Vec::is_empty")]
352 hint_rejections: Vec<WcetHintRejection>,
353 },
354}
355
356impl WcetFunction {
357 /// Construct a decline, filling in the note from the reason.
358 pub fn declined(name: impl Into<String>, reason: WcetDecline) -> Self {
359 let note = reason.note().to_string();
360 WcetFunction::Declined {
361 name: name.into(),
362 reason,
363 note,
364 hint_rejections: Vec::new(),
365 }
366 }
367
368 /// Construct a decline carrying rejected-hint records.
369 pub fn declined_with_rejections(
370 name: impl Into<String>,
371 reason: WcetDecline,
372 hint_rejections: Vec<WcetHintRejection>,
373 ) -> Self {
374 let note = reason.note().to_string();
375 WcetFunction::Declined {
376 name: name.into(),
377 reason,
378 note,
379 hint_rejections,
380 }
381 }
382}
383
384/// (#778 phase 4 / #49) A proven SELF-recursion certificate: the function is a
385/// single-self-call chain whose controlling value is entry-independently bounded
386/// (a masked-slot counter decreasing by a const step toward a base guard on the
387/// SAME masked quantity), so its maximum recursion DEPTH is DERIVED (not
388/// hint-supplied) as an entry-independent ceiling. The composer folds the self-edge
389/// as `frame_count × frame_cost` (`frame_count = max_depth + 1`, counting the base
390/// frame) instead of declining `Recursion`.
391///
392/// A certificate is attached ONLY after the depth was cross-checked against a
393/// `--wcet-hints` `recursion_depth` entry (the untrusted oracle asserts intent;
394/// synth's derived ceiling is what is emitted). Without a hint the recursion still
395/// declines (a bound this consequential is opt-in, mirroring the equality-exit
396/// loop-hint gate). `self_label` is the function's own `func_<idx>` self-call label
397/// so the composer can identify and special-case exactly that edge.
398#[derive(Debug, Clone, PartialEq, Eq)]
399pub struct WcetRecursionCert {
400 /// The self-call `BL` label (`func_<idx>`) this certificate authorizes.
401 pub self_label: String,
402 /// The DERIVED maximum recursion depth (entry-independent ceiling). The base
403 /// frame is NOT included here — the composer uses `max_depth + 1` frames.
404 pub max_depth: u64,
405 /// The hint value that gated this certificate (recorded for the sidecar; the
406 /// emitted depth is always the derived `max_depth`, never the raw hint).
407 pub hint: u64,
408}
409
410/// One direct call site inside a composable function (#778 phase 3). Records the
411/// callee's `BL` label (`func_<idx>` for a local/relocatable-import call) and the
412/// per-instruction execution-count multiplier of the `BL` (1 outside any loop; the
413/// enclosing loop's proven trip product when the call sits inside a proven counted
414/// loop, so a call in a loop is counted `trip` times, never once).
415#[derive(Debug, Clone, PartialEq, Eq)]
416pub struct WcetCallSite {
417 /// The `BL` target label as emitted by the selector (`func_<wasm_index>` for a
418 /// direct local/import call; any other label is a runtime helper → external).
419 pub callee_label: String,
420 /// The call site's worst-case execution count (product of enclosing proven loop
421 /// trip factors; 1 outside any loop). `u128` to survive deep nesting without
422 /// wrapping, matching the loop-multiplier domain.
423 pub multiplier: u128,
424}
425
426/// The per-function INTERMEDIATE result of the WCET pass BEFORE inter-procedural
427/// composition (#778 phase 3). The backend produces one of these per function; the
428/// module-level composer ([`crate::wcet`] consumers call `synth_backend::wcet_compose`)
429/// resolves each function's direct call sites against the whole module and emits the
430/// final [`WcetFunction`] (a composed bound, or a propagated/recursion/indirect
431/// decline).
432///
433/// Splitting the pass in two keeps composition a PURE function over already-decided
434/// per-function facts: `own_cycles` already prices every non-call instruction
435/// (including each `BL`'s branch overhead) at its proven execution count, so the
436/// composed total is `own_cycles + Σ_site multiplier_site × callee_total` — the
437/// per-site multiplier makes a call inside a proven loop sound by construction.
438#[derive(Debug, Clone, PartialEq, Eq)]
439pub enum WcetIntermediate {
440 /// The function declines for a reason INDEPENDENT of composition (an unproven
441 /// loop, an internal looped expansion, an unsupported core, an unresolved label
442 /// branch, an indirect call, or an unmodeled op). Carried straight through to a
443 /// [`WcetFunction::Declined`]; composition never rescues these.
444 Declined {
445 name: String,
446 reason: WcetDecline,
447 hint_rejections: Vec<WcetHintRejection>,
448 },
449 /// The function's own body is bounded; its final bound depends only on resolving
450 /// the recorded direct call sites against the module's other functions.
451 Composable {
452 name: String,
453 /// The summed worst-case cost of every instruction in the final stream
454 /// (each priced at its documented worst case × its proven execution-count
455 /// multiplier), INCLUDING each direct `BL`'s branch overhead. The callee
456 /// bodies are added by the composer via `call_sites`.
457 own_cycles: u64,
458 /// Number of ARM instructions summed (diagnostic, carried to the bound).
459 instr_count: usize,
460 /// The direct call sites to resolve at compose time.
461 call_sites: Vec<WcetCallSite>,
462 /// Proven loops inside this function (carried to the bound unchanged).
463 loops: Vec<WcetLoopBound>,
464 /// (#778 phase 4 / #49) A proven self-recursion certificate, when this
465 /// function is a bounded single-self-call chain with a verified depth hint.
466 /// The composer folds the self-edge as `(max_depth+1) × frame_cost` instead
467 /// of declining `Recursion`. `None` for a non-recursive function or an
468 /// unverifiable/unhinted recursion (which still declines).
469 recursion_cert: Option<WcetRecursionCert>,
470 /// Hints rejected while analyzing this function (carried to the bound).
471 hint_rejections: Vec<WcetHintRejection>,
472 },
473}
474
475impl WcetIntermediate {
476 /// The compiled function name this intermediate is for.
477 pub fn name(&self) -> &str {
478 match self {
479 WcetIntermediate::Declined { name, .. } | WcetIntermediate::Composable { name, .. } => {
480 name
481 }
482 }
483 }
484}
485
486/// The parsed `--wcet-hints` file (`synth-wcet-hints-v1`) — an UNTRUSTED oracle
487/// input (#778 phase 2, the scry integration seam). Per function, an ordered
488/// array of claimed loop-trip-count upper bounds, matched to loops by ascending
489/// head offset (entry N = N-th loop head in the function; `null` skips a loop).
490/// Every entry is soundly CHECKED before use: synth re-derives the loop's trip
491/// count from its own induction proof and consumes the hint only when the
492/// derived count is ≤ the hint. A wrong or unverifiable hint is rejected with a
493/// machine reason ([`WcetHintReject`]) — never trusted into a bound.
494#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
495pub struct WcetHints {
496 /// Must equal [`HINTS_SCHEMA`].
497 pub schema: String,
498 /// Per-function hint arrays, keyed by the compiled function name.
499 #[serde(default)]
500 pub functions: std::collections::BTreeMap<String, WcetFunctionHints>,
501}
502
503/// Per-function loop-bound hints.
504#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
505pub struct WcetFunctionHints {
506 /// Claimed trip-count upper bounds, one per loop in ascending-head-offset
507 /// order; `null` leaves that loop unhinted.
508 #[serde(default)]
509 pub loop_bounds: Vec<Option<u64>>,
510 /// (#778 phase 4 / #49) An UNTRUSTED claimed maximum SELF-recursion depth for
511 /// this function. Consulted only when synth has proven the function is a
512 /// single-self-call chain whose controlling value is entry-independently bounded
513 /// (a masked-slot counter): synth then DERIVES its own maximum depth from the
514 /// mask+step+base induction and cross-checks this hint (`hint < derived` →
515 /// `hint-below-derived-depth`). A hint on a function whose recursion synth cannot
516 /// so verify is REJECTED (`hint-unverifiable-recursion`) and never trusted. The
517 /// emitted bound always uses synth's DERIVED depth, never the raw hint.
518 #[serde(default, skip_serializing_if = "Option::is_none")]
519 pub recursion_depth: Option<u64>,
520}
521
522/// The full `synth-wcet-v1` sidecar: schema header, precondition, and per-function
523/// bounds/declines.
524#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
525pub struct WcetReport {
526 /// Schema version (`synth-wcet-v1`).
527 pub schema: String,
528 /// The compiled module name (for diagnostics).
529 pub module: String,
530 /// The core class the cycle table is written for (e.g. `"cortex-m4"`). The
531 /// bound is CONDITIONAL on this core.
532 pub core_class: String,
533 /// Assumed instruction-memory wait states (0 for the sound zero-wait table).
534 pub wait_states: u32,
535 /// Human statement of the memory precondition the bound holds under.
536 pub memory_assumption: String,
537 /// Per-function bound or decline. Complete: one entry per compiled function.
538 pub functions: Vec<WcetFunction>,
539}
540
541impl WcetReport {
542 /// Start an empty report for `module`, targeting `core_class` under the sound
543 /// zero-wait precondition.
544 pub fn new(module: impl Into<String>, core_class: impl Into<String>) -> Self {
545 WcetReport {
546 schema: SCHEMA.to_string(),
547 module: module.into(),
548 core_class: core_class.into(),
549 wait_states: 0,
550 memory_assumption:
551 "zero-wait-state instruction memory (flash accelerator / I-cache hit); \
552 in-order single-issue pipeline; documented per-instruction worst-case cycles"
553 .to_string(),
554 functions: Vec::new(),
555 }
556 }
557
558 /// Serialize to pretty JSON.
559 pub fn to_json(&self) -> serde_json::Result<String> {
560 serde_json::to_string_pretty(self)
561 }
562
563 /// Resolve the sidecar path (`<output>.wcet.json`) next to the ELF output.
564 pub fn sidecar_path(output: &std::path::Path) -> std::path::PathBuf {
565 let mut s = output.as_os_str().to_os_string();
566 s.push(".wcet.json");
567 std::path::PathBuf::from(s)
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574
575 #[test]
576 fn bounded_and_declined_roundtrip() {
577 let mut r = WcetReport::new("m", "cortex-m4");
578 r.functions.push(WcetFunction::Bounded {
579 name: "leaf".into(),
580 cycles: 42,
581 instr_count: 7,
582 loops: Vec::new(),
583 recursion: None,
584 hint_rejections: Vec::new(),
585 });
586 r.functions
587 .push(WcetFunction::declined("spins", WcetDecline::Loop));
588 let json = r.to_json().unwrap();
589 let back: WcetReport = serde_json::from_str(&json).unwrap();
590 assert_eq!(r, back);
591 // Decline reason is machine-readable and carries a note.
592 assert!(json.contains("\"reason\": \"loop\""));
593 assert!(json.contains("synth-wcet-v1"));
594 }
595
596 #[test]
597 fn sidecar_path_appends_suffix() {
598 let p = WcetReport::sidecar_path(std::path::Path::new("out/app.elf"));
599 assert_eq!(p, std::path::PathBuf::from("out/app.elf.wcet.json"));
600 }
601}