Skip to main content

vj_sim/sva/
trace.rs

1//! T11 (`docs/superpowers/plans/2026-10-03-sva.md`): a reference interpreter
2//! for a built [`Automaton`] over a CONCRETE trace -- the oracle the sequence
3//! operator tests check against wherever a pure word language cannot express
4//! the semantics (`first_match`, which selects matches by END TICK across a
5//! whole evaluation attempt), and the one place the
6//! [`Transition::FirstMatchBegin`]/[`Transition::FirstMatchEnd`] contract is
7//! executable. The runtime engine (`vj-runtime::sva_engine`, a later task)
8//! is a separate, incremental implementation of the same contract; this one is
9//! a plain tick-ordered search that favors being obviously right over being
10//! fast, and is never on a simulation path.
11//!
12//! One call is one EVALUATION ATTEMPT (16.12, 16.9.3): the sequence starts at
13//! tick `start`, `leaf(id, tick)` says whether Boolean leaf `id` holds on
14//! `tick`, and the result is every tick a match ends on. Ticks run `0..len`;
15//! a path that would leave that window is dropped (a sequence cannot match on
16//! a tick the trace never reached).
17use std::collections::{BTreeMap, BTreeSet, HashMap};
18use std::fmt;
19
20use super::ir::{
21    Automaton, CallArg, CallId, ClockId, EndMethod, EndPoint, FirstMatchId, LeafId, LocalVarId,
22    ThreadStateId, Transition,
23};
24use super::wide;
25
26/// What one attempt of an automaton over a trace produced.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct Attempt {
29    /// The automaton's EMPTY match (16.9.2.1), copied through: it spans no
30    /// tick, so it has no end tick to put in `ends`.
31    pub admits_empty: bool,
32    /// Every tick a NONEMPTY match of the sequence ends on.
33    pub ends: BTreeSet<u32>,
34}
35
36/// One thread's local-variable frame (T13): slot `i` is the value of
37/// [`LocalVarId`]`(i)`, `None` while the variable is unassigned (16.10).
38pub type Frame = Vec<Option<i64>>;
39
40/// The matches of one sequence evaluation, `(end tick, frame)`. Almost every
41/// evaluation of a bounded or single-letter antecedent has at most one, so the
42/// first lives inline: no heap allocation per attempt tick.
43pub type Matches = smallvec::SmallVec<[(u32, Frame); 1]>;
44
45/// What one attempt of an automaton produced when its local-variable frames
46/// are tracked (see [`attempt_with_locals`]).
47#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct LocalAttempt {
49    /// The automaton's empty match, copied through as in [`Attempt`].
50    pub admits_empty: bool,
51    /// Every nonempty match as `(end tick, the matching thread's frame)`: one
52    /// entry per distinct pair, since several threads may match on the same
53    /// tick with different valuations (16.10 rule (c)).
54    pub matches: BTreeSet<(u32, Frame)>,
55}
56
57impl LocalAttempt {
58    /// The end ticks alone, the shape [`Attempt::ends`] has.
59    pub fn ends(&self) -> BTreeSet<u32> {
60        self.matches.iter().map(|(t, _)| *t).collect()
61    }
62}
63
64/// One subroutine call a thread scheduled at a match item (16.11, T14): the
65/// tick of the end point it fired on, which call site, and each actual
66/// argument's value (`None` for a by-reference argument, which has no value of
67/// its own: it names the actual).
68#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
69pub struct CallEvent {
70    pub tick: u32,
71    pub call: CallId,
72    pub args: Vec<Option<i64>>,
73}
74
75/// What one attempt produced when both local-variable frames and subroutine
76/// calls are tracked (see [`attempt_with_calls`]).
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct CallAttempt {
79    /// The matches, as in [`LocalAttempt`].
80    pub attempt: LocalAttempt,
81    /// Every distinct call a thread scheduled, whether or not that thread
82    /// later matched: a match item fires at every end point of the sequence it
83    /// is attached to (16.11), even when a longer sequence it is part of then
84    /// fails.
85    pub calls: BTreeSet<CallEvent>,
86}
87
88/// An automaton shape this interpreter refuses to run, never a guess.
89#[derive(Clone, Debug, PartialEq, Eq)]
90pub enum TraceError {
91    /// An edge kind with no meaning outside the full engine (abort edges
92    /// belong to T20 and later; the local-variable edges are only interpreted
93    /// by [`attempt_with_locals`], which has a frame to run them on).
94    UnsupportedEdge(&'static str),
95    /// A leaf read local variable `var` while it was unassigned (16.10): the
96    /// builder rejects every read it can prove unassigned, so this is a
97    /// malformed automaton or an evaluator reading a slot it was not given.
98    UnassignedRead(LocalVarId),
99    /// A [`Transition::FirstMatchEnd`] reached by a thread that never took the
100    /// group's [`Transition::FirstMatchBegin`]: a malformed automaton.
101    EndWithoutBegin(FirstMatchId),
102    /// A [`Transition::FirstMatchBegin`] reached while the group is already
103    /// open on the same thread: a malformed automaton (a region cannot
104    /// contain itself).
105    BeginWhileOpen(FirstMatchId),
106    /// A `.matched` end point run without a clock model: its status depends on
107    /// the two clocks' tick times, which only [`attempt_with_clocks`] has.
108    MatchedWithoutClocks,
109    /// A `.matched` end point inside an instance that is itself being run in
110    /// another clock's tick space: the model has one destination and one source
111    /// clock, so a third is refused rather than guessed.
112    NestedMatched,
113    /// The clock model's tick times are not strictly increasing.
114    BadClocks,
115    /// A [`Timeline`]'s tick list is not strictly increasing, or names a step at
116    /// or past the trace's real length.
117    BadTimeline,
118    /// [`crate::sva::property::Evaluator::root_sequence_matches`] on a property
119    /// whose root is not a bare sequence (a `cover sequence` always has one).
120    NotASequence,
121}
122
123impl fmt::Display for TraceError {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        match self {
126            TraceError::NotASequence => write!(
127                f,
128                "sva trace interpreter: the matches of a property whose root is not a sequence \
129                 were asked for"
130            ),
131            TraceError::BadTimeline => write!(
132                f,
133                "sva trace interpreter: a timeline's tick lists must be strictly increasing and \
134                 inside the trace"
135            ),
136            TraceError::MatchedWithoutClocks => write!(
137                f,
138                "sva trace interpreter: a `.matched` end point needs a clock model \
139                 (attempt_with_clocks)"
140            ),
141            TraceError::NestedMatched => write!(
142                f,
143                "sva trace interpreter: a `.matched` inside an instance run on another clock is \
144                 not modelled"
145            ),
146            TraceError::BadClocks => write!(
147                f,
148                "sva trace interpreter: the clock model's tick times must be strictly increasing"
149            ),
150            TraceError::UnsupportedEdge(kind) => {
151                write!(f, "sva trace interpreter: {kind} edges are not interpreted")
152            }
153            TraceError::EndWithoutBegin(g) => write!(
154                f,
155                "sva trace interpreter: FirstMatchEnd({}) reached without its FirstMatchBegin",
156                g.0
157            ),
158            TraceError::BeginWhileOpen(g) => write!(
159                f,
160                "sva trace interpreter: FirstMatchBegin({}) reached while the group is open",
161                g.0
162            ),
163            TraceError::UnassignedRead(v) => write!(
164                f,
165                "sva trace interpreter: read of unassigned local variable {}",
166                v.0
167            ),
168        }
169    }
170}
171
172impl std::error::Error for TraceError {}
173
174/// The value of local variable `var` in `frame`, or
175/// [`TraceError::UnassignedRead`] while it is unassigned. The accessor a
176/// locals-aware leaf evaluator reads a variable through.
177pub fn read_local(frame: &[Option<i64>], var: LocalVarId) -> Result<i64, TraceError> {
178    frame
179        .get(var.0 as usize)
180        .copied()
181        .flatten()
182        .ok_or(TraceError::UnassignedRead(var))
183}
184
185/// One thread: `(tick, state, open first_match regions as (group, begin
186/// tick), sorted by group, local-variable frame)`. Tick first so the work set
187/// pops in tick order.
188///
189/// `ClockId` is the thread's CURRENT clock (multiclock sequences, 16.13): it moves
190/// only along a [`Transition::ClockSync`], and a [`Transition::Delay`] counts
191/// ticks of it.
192type Config = (u32, u32, ClockId, Vec<(FirstMatchId, u32)>, Frame);
193
194/// The evaluator both entry points feed [`run`]: a leaf's value at a tick
195/// given the thread's frame.
196type Eval<'a> = &'a dyn Fn(LeafId, u32, &[Option<i64>]) -> Result<i64, TraceError>;
197
198/// Runs one attempt: see the module doc comment. A leaf here is a pure
199/// Boolean, so an automaton with local-variable edges is refused (see
200/// [`attempt_with_locals`]).
201///
202/// Configurations are processed strictly in tick order (a `BTreeSet` popped
203/// from the front), so every `FirstMatchEnd` at tick `t` is seen before any at
204/// a later tick -- exactly the order the engine's lock-step stepping gives it,
205/// and the property the first-match table's "record the first end tick seen"
206/// rule rests on.
207pub fn attempt(
208    au: &Automaton,
209    start: u32,
210    len: u32,
211    leaf: &dyn Fn(LeafId, u32) -> bool,
212) -> Result<Attempt, TraceError> {
213    let matches = run(
214        au,
215        start,
216        len,
217        &|id, tick, _| Ok(i64::from(leaf(id, tick))),
218        false,
219    )?;
220    Ok(Attempt {
221        admits_empty: au.admits_empty,
222        ends: matches.0.into_iter().map(|(t, _)| t).collect(),
223    })
224}
225
226/// [`attempt`] with the local-variable frame (T13): every thread carries a
227/// [`Frame`] that starts all-unassigned, a [`Transition::LocalVarWrite`]
228/// stores the cast value of its `value` leaf, a [`Transition::LocalVarCopy`]
229/// copies (casting, or propagating "unassigned"), and a fork copies the frame.
230///
231/// `eval(leaf, tick, frame)` is the VALUE of a leaf at a tick given the
232/// thread's frame: a guard holds when it is nonzero and a write stores it. An
233/// evaluator reads a local variable through [`read_local`], so a read of an
234/// unassigned variable fails the whole attempt with
235/// [`TraceError::UnassignedRead`] instead of guessing a value.
236pub fn attempt_with_locals(
237    au: &Automaton,
238    start: u32,
239    len: u32,
240    eval: Eval<'_>,
241) -> Result<LocalAttempt, TraceError> {
242    Ok(LocalAttempt {
243        admits_empty: au.admits_empty,
244        matches: run(au, start, len, eval, true)?.0,
245    })
246}
247
248/// [`attempt_with_locals`] that also reports the subroutine calls (16.11) the
249/// attempt scheduled. A [`Transition::SubroutineCall`] is a same-tick,
250/// unguarded pass-through whose only effect is its [`CallEvent`], recorded the
251/// moment a thread takes the edge: a call is not conditional on the sequence
252/// ultimately matching.
253pub fn attempt_with_calls(
254    au: &Automaton,
255    start: u32,
256    len: u32,
257    eval: Eval<'_>,
258) -> Result<CallAttempt, TraceError> {
259    let (matches, calls) = run(au, start, len, eval, true)?;
260    Ok(CallAttempt {
261        attempt: LocalAttempt {
262            admits_empty: au.admits_empty,
263            matches,
264        },
265        calls,
266    })
267}
268
269/// [`attempt_with_calls`] over two clock domains (16.13.5, 16.13.6): the trace is
270/// `clocks.dest.len()` ticks of the clock the automaton runs on, and a
271/// `.matched` end point inside it is evaluated against ticks of the clock of
272/// the instance it is applied to (`clocks.source`). `eval(leaf, tick, frame)` is
273/// called with a tick of the leaf's OWN clock -- source ticks for the leaves
274/// of an instance a `.matched` is applied to, destination ticks for the rest --
275/// and it is the evaluator that knows which leaves are which.
276///
277/// Only the calls of instances whose end point some thread actually tested are
278/// reported.
279pub fn attempt_with_clocks(
280    au: &Automaton,
281    start: u32,
282    clocks: &TraceClocks,
283    eval: Eval<'_>,
284) -> Result<CallAttempt, TraceError> {
285    let strictly_increasing = |v: &[u64]| v.windows(2).all(|w| w[0] < w[1]);
286    if !strictly_increasing(&clocks.dest) || !strictly_increasing(&clocks.source) {
287        return Err(TraceError::BadClocks);
288    }
289    let mut runner = Runner {
290        eval,
291        locals: true,
292        len: clocks.dest.len() as u32,
293        clocks: Some(clocks),
294        timeline: None,
295        status: HashMap::new(),
296        calls: BTreeSet::new(),
297        word: None,
298    };
299    let matches = runner.attempt(au, start, Space::Dest)?;
300    Ok(CallAttempt {
301        attempt: LocalAttempt {
302            admits_empty: au.admits_empty,
303            matches,
304        },
305        calls: runner.calls,
306    })
307}
308
309/// A finite trace completed with a constant tail, the infinite words Annex F's
310/// satisfaction relations are defined over (F.5.3.2): `w` followed by `⊤^ω` is
311/// the WEAK verdict word (nothing has failed yet), `w` followed by `⊥^ω` the
312/// STRONG one (everything still owed has failed). A letter `⊤` satisfies every
313/// Boolean expression, a letter `⊥` none -- not even `1'b1` (F.5.3).
314///
315/// Ticks `0..real` are the trace's real samples (a leaf is evaluated there);
316/// then come the explicit `mid` letters (`true` is `⊤`), then `tail_top`
317/// forever. `mid` exists because a `⊥`-tailed word cut at a later tick and
318/// completed with `⊤` (`w^{0,i-1}⊤^ω`, accept_on/weak) has a run of `⊥`
319/// letters before its `⊤` tail.
320#[derive(Clone, Debug, PartialEq, Eq, Hash)]
321pub struct TailWord {
322    pub real: u32,
323    pub mid: Vec<bool>,
324    pub tail_top: bool,
325}
326
327impl TailWord {
328    /// `w⊤^ω` over `real` real samples: the weak verdict word.
329    pub fn weak(real: u32) -> Self {
330        TailWord {
331            real,
332            mid: Vec::new(),
333            tail_top: true,
334        }
335    }
336
337    /// `w⊥^ω` over `real` real samples: the strong verdict word.
338    pub fn strong(real: u32) -> Self {
339        TailWord {
340            real,
341            mid: Vec::new(),
342            tail_top: false,
343        }
344    }
345
346    /// The first tick whose letter is the constant tail: every later tick looks
347    /// the same, so a property cannot tell tick `extent()` from any tick after.
348    pub fn extent(&self) -> u32 {
349        self.real + self.mid.len() as u32
350    }
351
352    /// `Some(true)` for a `⊤` letter, `Some(false)` for `⊥`, `None` for a real
353    /// sample at `tick`.
354    pub fn letter(&self, tick: u32) -> Option<bool> {
355        if tick < self.real {
356            None
357        } else {
358            Some(
359                self.mid
360                    .get((tick - self.real) as usize)
361                    .copied()
362                    .unwrap_or(self.tail_top),
363            )
364        }
365    }
366
367    /// The dual word `w̄` (F.5.3.1: `w ⊨ not P` iff `w̄ ⊭ P`): every `⊤` letter
368    /// becomes `⊥` and vice versa. The real samples are untouched.
369    pub fn flip(&self) -> Self {
370        TailWord {
371            real: self.real,
372            mid: self.mid.iter().map(|t| !t).collect(),
373            tail_top: !self.tail_top,
374        }
375    }
376
377    /// `w^{0,n-1}⊤^ω`: the first `n` letters, then `⊤` forever (the word
378    /// `weak(R)` and `accept_on` complete a prefix with, F.5.3.1).
379    pub fn cut_then_top(&self, n: u32) -> Self {
380        if n <= self.real {
381            return TailWord::weak(n);
382        }
383        let keep = (n - self.real) as usize;
384        let mid = (0..keep)
385            .map(|i| self.mid.get(i).copied().unwrap_or(self.tail_top))
386            .collect();
387        let mut w = TailWord {
388            real: self.real,
389            mid,
390            tail_top: true,
391        };
392        // Trailing `⊤` letters are the tail already: one canonical spelling.
393        while w.mid.last() == Some(&true) {
394            w.mid.pop();
395        }
396        w
397    }
398}
399
400/// The instants each clock of a multiclock assertion ticks at (16.13), over ONE
401/// shared axis of positions (the simulation's time steps, or the trace's rows).
402/// Position `p` is a tick of clock `c` when `p` is in `c`'s list.
403///
404/// Positions `0..real` are the trace's real steps. From `real` on, a word is the
405/// constant `⊤`/`⊥` tail of [`TailWord`], which is a letter at EVERY position, so
406/// every position past `real` is a tick of every clock: the tail neither adds nor
407/// removes obligations clock by clock, it simply continues the verdict word. A
408/// clock with no list at all ticks at every position (the single-clock case, where
409/// a timeline is not needed).
410#[derive(Clone, Debug, PartialEq, Eq)]
411pub struct Timeline {
412    real: u32,
413    ticks: Vec<Vec<u32>>,
414}
415
416impl Timeline {
417    /// `ticks[c]` is the sorted tick list of `ClockId(c)`; every entry must be
418    /// below `real`.
419    pub fn new(real: u32, ticks: Vec<Vec<u32>>) -> Result<Self, TraceError> {
420        let ok = ticks
421            .iter()
422            .all(|l| l.windows(2).all(|w| w[0] < w[1]) && l.iter().all(|&t| t < real));
423        if ok {
424            Ok(Timeline { real, ticks })
425        } else {
426            Err(TraceError::BadTimeline)
427        }
428    }
429
430    /// One tick list per row of `rows`: step `t` is a tick of clock `c` when
431    /// `rows[c][t]`. The timeline is as long as the longest row.
432    pub fn from_rows(rows: &[&[bool]]) -> Self {
433        let real = rows.iter().map(|r| r.len() as u32).max().unwrap_or(0);
434        let ticks = rows
435            .iter()
436            .map(|r| {
437                r.iter()
438                    .enumerate()
439                    .filter(|&(_, &b)| b)
440                    .map(|(t, _)| t as u32)
441                    .collect()
442            })
443            .collect();
444        Timeline { real, ticks }
445    }
446
447    /// The number of real steps.
448    pub fn real(&self) -> u32 {
449        self.real
450    }
451
452    /// The first `n` real steps: the timeline of a word cut to `n` letters, whose
453    /// later positions are the constant tail (a tick of every clock). A cut
454    /// longer than the trace is the trace.
455    pub fn truncated(&self, n: u32) -> Timeline {
456        let real = n.min(self.real);
457        Timeline {
458            real,
459            ticks: self
460                .ticks
461                .iter()
462                .map(|l| l.iter().copied().take_while(|&t| t < real).collect())
463                .collect(),
464        }
465    }
466
467    fn list(&self, c: ClockId) -> Option<&[u32]> {
468        self.ticks.get(c.0 as usize).map(Vec::as_slice)
469    }
470
471    /// Is `p` a tick of `c`?
472    pub fn is_tick(&self, c: ClockId, p: u32) -> bool {
473        match self.list(c) {
474            _ if p >= self.real => true,
475            None => true,
476            Some(l) => l.binary_search(&p).is_ok(),
477        }
478    }
479
480    /// The first tick of `c` at or after `p` (`p` itself when it is one).
481    pub fn next_tick(&self, c: ClockId, p: u32) -> u32 {
482        match self.list(c) {
483            _ if p >= self.real => p,
484            None => p,
485            Some(l) => l
486                .get(l.partition_point(|&t| t < p))
487                .copied()
488                .unwrap_or(self.real),
489        }
490    }
491
492    /// The `k`-th tick of `c` strictly after `p` (`k == 0` is `p` itself: the
493    /// caller is already standing on a tick). `##1` is `k == 1`: "the nearest
494    /// strictly subsequent tick" (16.13.1).
495    pub fn kth_after(&self, c: ClockId, p: u32, k: u32) -> u32 {
496        if k == 0 {
497            return p;
498        }
499        match self.list(c) {
500            _ if p >= self.real => p + k,
501            None => p + k,
502            Some(l) => {
503                let at_or_before = l.partition_point(|&t| t <= p);
504                let j = at_or_before + k as usize - 1;
505                match l.get(j) {
506                    Some(&t) => t,
507                    None => self.real + (j - l.len()) as u32,
508                }
509            }
510        }
511    }
512}
513
514/// One evaluation attempt of `au` from `entry` with initial frame `frame0`,
515/// started at tick `start`, over the tail-completed `word` (see [`TailWord`]).
516/// Ticks `0..horizon` are searched; `horizon` must reach as far as any match can
517/// matter (a property evaluator derives it from the automaton, see
518/// `property::horizon`). A leaf on a `⊤`/`⊥` letter is that letter: guards hold
519/// on `⊤` only, a value read there is `0`, and no subroutine call is scheduled.
520///
521/// Every match is `(end tick, frame)`, as in [`LocalAttempt`]; the empty match
522/// is the caller's to track (it is a flag of the automaton, not of a path).
523pub fn attempt_word(
524    au: &Automaton,
525    entry: ThreadStateId,
526    frame0: &[Option<i64>],
527    start: u32,
528    word: &TailWord,
529    horizon: u32,
530    eval: Eval<'_>,
531) -> Result<BTreeSet<(u32, Frame)>, TraceError> {
532    attempt_word_in(
533        au,
534        entry,
535        frame0,
536        start,
537        word,
538        horizon,
539        eval,
540        None,
541        au.entry_clock,
542    )
543}
544
545/// [`attempt_word`] over a multiclock [`Timeline`] (`None`: every position is a
546/// tick of every clock). The thread starts in clock `dom` and `start` is moved to
547/// the first tick of `dom` at or after it. A `.matched` end point is refused
548/// here exactly as in [`attempt_word`]: its two-space clock model is
549/// [`attempt_with_clocks`]'s.
550#[allow(clippy::too_many_arguments)]
551pub fn attempt_word_in(
552    au: &Automaton,
553    entry: ThreadStateId,
554    frame0: &[Option<i64>],
555    start: u32,
556    word: &TailWord,
557    horizon: u32,
558    eval: Eval<'_>,
559    timeline: Option<&Timeline>,
560    dom: ClockId,
561) -> Result<BTreeSet<(u32, Frame)>, TraceError> {
562    attempt_word_calls(au, entry, frame0, start, word, horizon, eval, timeline, dom)
563        .map(|(matches, _)| matches)
564}
565
566/// [`attempt_word_in`] that also returns every subroutine call a thread
567/// scheduled (16.11) on a REAL tick of `word`: a call reached on a `⊤`/`⊥`
568/// letter is not a call of the simulation, as no sample was taken there. A call
569/// is listed whether or not the thread that scheduled it later matched.
570#[allow(clippy::too_many_arguments)]
571pub fn attempt_word_calls(
572    au: &Automaton,
573    entry: ThreadStateId,
574    frame0: &[Option<i64>],
575    start: u32,
576    word: &TailWord,
577    horizon: u32,
578    eval: Eval<'_>,
579    timeline: Option<&Timeline>,
580    dom: ClockId,
581) -> Result<RunResult, TraceError> {
582    let mut runner = Runner {
583        eval,
584        locals: true,
585        len: horizon,
586        clocks: None,
587        timeline,
588        status: HashMap::new(),
589        calls: BTreeSet::new(),
590        word: Some(word),
591    };
592    let matches = runner.attempt_from(au, entry, frame0.to_vec(), start, dom, Space::Dest)?;
593    Ok((matches, runner.calls))
594}
595
596/// What [`run`] finds: every `(end tick, frame)` a match ends with, and every
597/// distinct call a thread scheduled.
598pub type RunResult = (BTreeSet<(u32, Frame)>, BTreeSet<CallEvent>);
599
600/// The shared search behind [`attempt`], [`attempt_with_locals`] and
601/// [`attempt_with_calls`]. `locals` says whether the local-variable,
602/// subroutine-call and end-point edges are interpreted or refused.
603fn run(
604    au: &Automaton,
605    start: u32,
606    len: u32,
607    eval: Eval<'_>,
608    locals: bool,
609) -> Result<RunResult, TraceError> {
610    let mut runner = Runner {
611        eval,
612        locals,
613        len,
614        clocks: None,
615        timeline: None,
616        status: HashMap::new(),
617        calls: BTreeSet::new(),
618        word: None,
619    };
620    let matches = runner.attempt(au, start, Space::Dest)?;
621    Ok((matches, runner.calls))
622}
623
624/// The tick-time model [`attempt_with_clocks`] runs a `.matched` end point
625/// (16.13.5) under: the simulation time of every tick of the clock of the
626/// automaton being run (`dest`, the clock of the context that tests the method)
627/// and of the clock of the instance the method is applied to (`source`).
628/// Both are strictly increasing.
629#[derive(Clone, Debug, PartialEq, Eq)]
630pub struct TraceClocks {
631    pub dest: Vec<u64>,
632    pub source: Vec<u64>,
633}
634
635/// Which clock's ticks a run counts in: the tested automaton's own (`Dest`), or
636/// the clock of the instance a `.matched` is applied to (`Source`).
637#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
638enum Space {
639    Dest,
640    Source,
641}
642
643/// What an end point's instance produced, by end tick: the distinct frames its
644/// attempts matched with there.
645type EndStatus = BTreeMap<u32, BTreeSet<Frame>>;
646
647/// One interpretation: the leaf evaluator, the clock model if any, and the two
648/// pieces of state that outlive a single attempt -- the memoized free-running
649/// status of every end-point instance, and the calls scheduled so far.
650struct Runner<'a> {
651    eval: Eval<'a>,
652    locals: bool,
653    /// Length of the [`Space::Dest`] trace.
654    len: u32,
655    clocks: Option<&'a TraceClocks>,
656    /// The per-clock tick lists of a multiclock run (16.13), or `None` when every
657    /// position is a tick of every clock. Only [`Space::Dest`] runs consult it.
658    timeline: Option<&'a Timeline>,
659    /// Keyed by the end point's identity (its address inside the automaton being
660    /// run, stable for the whole run) and the space it was evaluated in.
661    status: HashMap<(usize, Space), EndStatus>,
662    calls: BTreeSet<CallEvent>,
663    /// The tail-completed word the run is over (see [`TailWord`]), or `None` for
664    /// a plain trace: ticks from `word.real` on are ⊤/⊥ letters, not samples.
665    word: Option<&'a TailWord>,
666}
667
668impl Runner<'_> {
669    fn len_of(&self, space: Space) -> u32 {
670        match space {
671            Space::Dest => self.len,
672            Space::Source => self.clocks.map_or(0, |c| c.source.len() as u32),
673        }
674    }
675
676    /// Every attempt of `au` started at every tick of `space`: what a
677    /// free-running sequence instance does (16.9.11, "does not depend upon the
678    /// starting point").
679    fn free_run(&mut self, au: &Automaton, space: Space) -> Result<EndStatus, TraceError> {
680        let mut out = EndStatus::new();
681        for start in 0..self.len_of(space) {
682            // An attempt starts at a tick of the instance's own clock only.
683            if space == Space::Dest
684                && self
685                    .timeline
686                    .is_some_and(|t| !t.is_tick(au.entry_clock, start))
687            {
688                continue;
689            }
690            for (end, frame) in self.attempt(au, start, space)? {
691                out.entry(end).or_default().insert(frame);
692            }
693        }
694        Ok(out)
695    }
696
697    /// The status of `ep`'s instance for a run in `space`, computed once.
698    fn end_status(&mut self, ep: &EndPoint, space: Space) -> Result<&EndStatus, TraceError> {
699        let key = (std::ptr::from_ref(ep) as usize, space);
700        if !self.status.contains_key(&key) {
701            let computed = self.compute_status(ep, space)?;
702            self.status.insert(key, computed);
703        }
704        Ok(&self.status[&key])
705    }
706
707    fn compute_status(&mut self, ep: &EndPoint, space: Space) -> Result<EndStatus, TraceError> {
708        match (ep.method, space) {
709            // Same clock: the instance's end ticks are this run's ticks.
710            (EndMethod::Triggered, _) => self.free_run(&ep.automaton, space),
711            (EndMethod::Matched, Space::Source) => Err(TraceError::NestedMatched),
712            (EndMethod::Matched, Space::Dest) => {
713                let Some(clocks) = self.clocks else {
714                    // Over a shared timeline: the instance's raw end points, at
715                    // ticks of its own clock; the reading edge gathers the ones
716                    // since its own clock's previous tick (the handoff below).
717                    return self.free_run(&ep.automaton, Space::Dest);
718                };
719                let source = self.free_run(&ep.automaton, Space::Source)?;
720                let mut out = EndStatus::new();
721                for (d, &hi) in clocks.dest.iter().enumerate() {
722                    // The one-slot handoff: a testing tick sees every match
723                    // since the previous testing tick, including one at its own
724                    // time (see `EndMethod::Matched`).
725                    let lo = d.checked_sub(1).map(|p| clocks.dest[p]);
726                    let mut frames = BTreeSet::new();
727                    for (&end, fs) in &source {
728                        let t = clocks.source[end as usize];
729                        if t <= hi && lo.is_none_or(|l| t > l) {
730                            frames.extend(fs.iter().cloned());
731                        }
732                    }
733                    if !frames.is_empty() {
734                        out.insert(d as u32, frames);
735                    }
736                }
737                Ok(out)
738            }
739        }
740    }
741
742    /// One evaluation attempt of `au` started at tick `start` of `space`.
743    fn attempt(
744        &mut self,
745        au: &Automaton,
746        start: u32,
747        space: Space,
748    ) -> Result<BTreeSet<(u32, Frame)>, TraceError> {
749        // A fresh attempt gets a fresh frame: no variable has a value carried
750        // over from a previous attempt (16.10).
751        let frame = vec![None; au.local_vars.len()];
752        self.attempt_from(au, au.start, frame, start, au.entry_clock, space)
753    }
754
755    /// [`Runner::attempt`] from an arbitrary entry state and initial frame: the
756    /// shape a property evaluator needs, where one automaton holds many
757    /// sequences' entries and a consequent's sequence starts with the frame the
758    /// antecedent left behind.
759    fn attempt_from(
760        &mut self,
761        au: &Automaton,
762        entry: ThreadStateId,
763        frame0: Frame,
764        start: u32,
765        dom0: ClockId,
766        space: Space,
767    ) -> Result<BTreeSet<(u32, Frame)>, TraceError> {
768        let (raw_eval, locals, len, word) = (self.eval, self.locals, self.len_of(space), self.word);
769        // Only the run in the automaton's own space has a timeline.
770        let timeline = if space == Space::Dest {
771            self.timeline
772        } else {
773            None
774        };
775        // The position of the `k`-th tick of `dom` after `from` (`k == 0`: `from`).
776        let step = |dom: ClockId, from: u32, k: u32| -> u32 {
777            timeline.map_or(from + k, |t| t.kth_after(dom, from, k))
778        };
779        let start = timeline.map_or(start, |t| t.next_tick(dom0, start));
780        // Past the real samples a leaf is a ⊤/⊥ letter, not a value: a guard
781        // holds on ⊤ and never on ⊥ (F.5.3), and a value read there is an
782        // arbitrary constant (F.5.5: "any constant values of the type"), 0.
783        let letter = |tick: u32| -> Option<bool> {
784            if space == Space::Dest {
785                word.and_then(|w| w.letter(tick))
786            } else {
787                None
788            }
789        };
790        let eval = |leaf: LeafId, tick: u32, frame: &[Option<i64>]| -> Result<i64, TraceError> {
791            match letter(tick) {
792                Some(_) => Ok(0),
793                None => raw_eval(leaf, tick, frame),
794            }
795        };
796        let mut matches = BTreeSet::new();
797        // (group, begin tick) -> the earliest end tick recorded for it.
798        let mut table: BTreeMap<(FirstMatchId, u32), u32> = BTreeMap::new();
799        let mut seen: BTreeSet<Config> = BTreeSet::new();
800        let mut work: BTreeSet<Config> = BTreeSet::new();
801        if start < len {
802            work.insert((start, entry.0, dom0, Vec::new(), frame0));
803        }
804        while let Some((tick, state, dom, tags, frame)) = work.pop_first() {
805            if !seen.insert((tick, state, dom, tags.clone(), frame.clone())) {
806                continue;
807            }
808            let st = au.state(ThreadStateId(state));
809            for edge in &st.edges {
810                if let Some(g) = edge.guard {
811                    let holds = match letter(tick) {
812                        Some(top) => top,
813                        None => raw_eval(g, tick, &frame)? != 0,
814                    };
815                    if !holds {
816                        continue;
817                    }
818                }
819                let target = edge.target.0;
820                match &edge.label {
821                    Transition::Epsilon => {
822                        work.insert((tick, target, dom, tags.clone(), frame.clone()));
823                    }
824                    Transition::Delay { min, max } if timeline.is_none() => {
825                        let room = len - 1 - tick;
826                        let hi = max.map_or(room, |m| m.min(room));
827                        for k in *min..=hi {
828                            work.insert((tick + k, target, dom, tags.clone(), frame.clone()));
829                        }
830                    }
831                    // `##[min:max]` counts ticks of the thread's current clock
832                    // (16.13.1); the first position past the trace ends the window.
833                    Transition::Delay { min, max } => {
834                        let mut k = *min;
835                        while max.is_none_or(|m| k <= m) {
836                            let to = step(dom, tick, k);
837                            if to >= len {
838                                break;
839                            }
840                            work.insert((to, target, dom, tags.clone(), frame.clone()));
841                            k += 1;
842                        }
843                    }
844                    Transition::ClockSync { to, strict } => {
845                        let at = if *strict {
846                            step(*to, tick, 1)
847                        } else {
848                            timeline.map_or(tick, |t| t.next_tick(*to, tick))
849                        };
850                        if at < len {
851                            work.insert((at, target, *to, tags.clone(), frame.clone()));
852                        }
853                    }
854                    Transition::Match { .. } => {
855                        matches.insert((tick, frame.clone()));
856                    }
857                    Transition::FirstMatchBegin { group } => {
858                        if tags.iter().any(|(g, _)| g == group) {
859                            return Err(TraceError::BeginWhileOpen(*group));
860                        }
861                        let mut t2 = tags.clone();
862                        t2.push((*group, tick));
863                        t2.sort();
864                        work.insert((tick, target, dom, t2, frame.clone()));
865                    }
866                    Transition::FirstMatchEnd { group } => {
867                        let Some(pos) = tags.iter().position(|(g, _)| g == group) else {
868                            return Err(TraceError::EndWithoutBegin(*group));
869                        };
870                        let begin = tags[pos].1;
871                        let earliest = *table.entry((*group, begin)).or_insert(tick);
872                        if earliest == tick {
873                            let mut t2 = tags.clone();
874                            t2.remove(pos);
875                            work.insert((tick, target, dom, t2, frame.clone()));
876                        }
877                    }
878                    Transition::Abort { .. } => return Err(TraceError::UnsupportedEdge("Abort")),
879                    Transition::LocalVarWrite { var, value } => {
880                        if !locals {
881                            return Err(TraceError::UnsupportedEdge("LocalVarWrite"));
882                        }
883                        let v = au.local_var(*var).cast(eval(*value, tick, &frame)?);
884                        let mut f2 = frame.clone();
885                        f2[var.0 as usize] = Some(v);
886                        work.insert((tick, target, dom, tags.clone(), f2));
887                    }
888                    Transition::LocalVarCopy { dst, src } => {
889                        if !locals {
890                            return Err(TraceError::UnsupportedEdge("LocalVarCopy"));
891                        }
892                        let mut f2 = frame.clone();
893                        wide::copy_local(
894                            &mut f2,
895                            *dst,
896                            au.local_var(*dst),
897                            &frame,
898                            *src,
899                            au.local_var(*src),
900                        );
901                        work.insert((tick, target, dom, tags.clone(), f2));
902                    }
903                    Transition::LocalVarStep { var, delta } => {
904                        if !locals {
905                            return Err(TraceError::UnsupportedEdge("LocalVarStep"));
906                        }
907                        let decl = au.local_var(*var);
908                        let mut f2 = frame.clone();
909                        if decl.is_wide() {
910                            wide::step_local(&mut f2, *var, decl, *delta)
911                                .ok_or(TraceError::UnassignedRead(*var))?;
912                        } else {
913                            let cur = read_local(&frame, *var)?;
914                            f2[var.0 as usize] = Some(decl.cast(cur.wrapping_add(*delta)));
915                        }
916                        work.insert((tick, target, dom, tags.clone(), f2));
917                    }
918                    Transition::SubroutineCall { call, args } => {
919                        if !locals {
920                            return Err(TraceError::UnsupportedEdge("SubroutineCall"));
921                        }
922                        let mut vals = Vec::with_capacity(args.len());
923                        for a in args {
924                            vals.push(match a {
925                                CallArg::Value(leaf) => Some(eval(*leaf, tick, &frame)?),
926                                CallArg::Ref | CallArg::Const => None,
927                            });
928                        }
929                        // A call scheduled on a ⊤/⊥ letter is not a real call.
930                        if letter(tick).is_none() {
931                            self.calls.insert(CallEvent {
932                                tick,
933                                call: *call,
934                                args: vals,
935                            });
936                        }
937                        work.insert((tick, target, dom, tags.clone(), frame.clone()));
938                    }
939                    Transition::EndPoint { point, flag } => {
940                        if !locals {
941                            return Err(TraceError::UnsupportedEdge("EndPoint"));
942                        }
943                        if let Some(top) = letter(tick) {
944                            // `s.triggered` on a ⊤ letter holds like any Boolean
945                            // does there, and never on ⊥ (F.5.3).
946                            if let Some(slot) = flag {
947                                let mut f2 = frame.clone();
948                                f2[slot.0 as usize] = Some(i64::from(top));
949                                work.insert((tick, target, dom, tags.clone(), f2));
950                            } else if top {
951                                work.insert((tick, target, dom, tags.clone(), frame.clone()));
952                            }
953                            continue;
954                        }
955                        let ep = au.end_point(*point);
956                        // `.matched` over a shared timeline (16.13.5): the one-slot
957                        // handoff sees every end point since the previous tick of
958                        // the testing thread's clock, up to and including this one.
959                        let handoff = (ep.method == EndMethod::Matched && self.clocks.is_none())
960                            .then(|| {
961                                (0..tick)
962                                    .rev()
963                                    .find(|&p| self.timeline.is_none_or(|t| t.is_tick(dom, p)))
964                                    .map_or(0, |p| p + 1)
965                            });
966                        let status = self.end_status(ep, space)?;
967                        let frames: Vec<Frame> = match handoff {
968                            Some(lo) => status
969                                .range(lo..=tick)
970                                .flat_map(|(_, s)| s.iter().cloned())
971                                .collect::<BTreeSet<Frame>>()
972                                .into_iter()
973                                .collect(),
974                            None => status
975                                .get(&tick)
976                                .map(|s| s.iter().cloned().collect())
977                                .unwrap_or_default(),
978                        };
979                        if let Some(slot) = flag {
980                            // Inside a larger expression: the status is a value the
981                            // leaf reads, and the thread goes on either way.
982                            let mut f2 = frame.clone();
983                            f2[slot.0 as usize] = Some(i64::from(!frames.is_empty()));
984                            work.insert((tick, target, dom, tags.clone(), f2));
985                        } else {
986                            // A guard: one thread per distinct matching frame, each
987                            // taking the flowed-out variables with it.
988                            for sub in frames {
989                                let mut f2 = frame.clone();
990                                for &(from, to) in &ep.flow_out {
991                                    wide::copy_local(
992                                        &mut f2,
993                                        to,
994                                        au.local_var(to),
995                                        &sub,
996                                        from,
997                                        au.local_var(from),
998                                    );
999                                }
1000                                work.insert((tick, target, dom, tags.clone(), f2));
1001                            }
1002                        }
1003                    }
1004                }
1005            }
1006        }
1007        Ok(matches)
1008    }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013    use super::*;
1014    use crate::sva::ir::{AbortMode, LocalVarId};
1015
1016    /// `leaf(i, t)` for a table of per-leaf boolean rows.
1017    fn rows(table: &'static [&'static [bool]]) -> impl Fn(LeafId, u32) -> bool {
1018        move |id, t| {
1019            table[id.0 as usize]
1020                .get(t as usize)
1021                .copied()
1022                .unwrap_or(false)
1023        }
1024    }
1025
1026    /// `l0 ##1 l1`: guard l0, Delay{1,1}, guard l1, Match.
1027    fn a_then_b() -> Automaton {
1028        let mut a = Automaton::empty();
1029        let s0 = a.start;
1030        let (s1, s2, s3) = (a.new_state(), a.new_state(), a.new_state());
1031        a.add_edge(s0, Some(LeafId(0)), Transition::Epsilon, s1);
1032        a.add_edge(
1033            s1,
1034            None,
1035            Transition::Delay {
1036                min: 1,
1037                max: Some(1),
1038            },
1039            s2,
1040        );
1041        a.add_edge(s2, Some(LeafId(1)), Transition::Epsilon, s3);
1042        a.add_edge(s3, None, Transition::Match { actions: vec![] }, s3);
1043        a
1044    }
1045
1046    #[test]
1047    fn concatenation_ends_one_tick_after_the_start() {
1048        let a = a_then_b();
1049        let tr = rows(&[&[true, false, true], &[false, true, false]]);
1050        let at0 = attempt(&a, 0, 3, &tr).unwrap();
1051        assert_eq!(at0.ends, BTreeSet::from([1]));
1052        assert!(!at0.admits_empty);
1053        // From tick 1: `a` is false there.
1054        assert!(attempt(&a, 1, 3, &tr).unwrap().ends.is_empty());
1055        // From tick 2: `a` holds but there is no tick 3 to sample `b` on.
1056        assert!(attempt(&a, 2, 3, &tr).unwrap().ends.is_empty());
1057    }
1058
1059    #[test]
1060    fn delay_window_yields_every_end_tick_in_range() {
1061        let mut a = Automaton::empty();
1062        let s0 = a.start;
1063        let (s1, s2) = (a.new_state(), a.new_state());
1064        a.add_edge(
1065            s0,
1066            None,
1067            Transition::Delay {
1068                min: 1,
1069                max: Some(3),
1070            },
1071            s1,
1072        );
1073        a.add_edge(s1, Some(LeafId(0)), Transition::Epsilon, s2);
1074        a.add_edge(s2, None, Transition::Match { actions: vec![] }, s2);
1075        let tr = rows(&[&[false, true, false, true, true]]);
1076        assert_eq!(
1077            attempt(&a, 0, 5, &tr).unwrap().ends,
1078            BTreeSet::from([1, 3]),
1079            "tick 4 is outside the window, tick 2 fails the guard"
1080        );
1081    }
1082
1083    #[test]
1084    fn unbounded_delay_stops_at_the_end_of_the_trace() {
1085        let mut a = Automaton::empty();
1086        let s0 = a.start;
1087        let s1 = a.new_state();
1088        a.add_edge(s0, None, Transition::Delay { min: 0, max: None }, s1);
1089        a.add_edge(s1, None, Transition::Match { actions: vec![] }, s1);
1090        let tr = rows(&[]);
1091        assert_eq!(
1092            attempt(&a, 1, 4, &tr).unwrap().ends,
1093            BTreeSet::from([1, 2, 3])
1094        );
1095    }
1096
1097    /// `first_match(##[0:2] l0)` begun at `start`: the markers around a region
1098    /// that can match on three different ticks.
1099    fn first_of_window() -> Automaton {
1100        let mut a = Automaton::empty();
1101        let g = a.new_first_match_group();
1102        let s0 = a.start;
1103        let (s1, s2, s3, s4) = (a.new_state(), a.new_state(), a.new_state(), a.new_state());
1104        a.add_edge(s0, None, Transition::FirstMatchBegin { group: g }, s1);
1105        a.add_edge(
1106            s1,
1107            None,
1108            Transition::Delay {
1109                min: 0,
1110                max: Some(2),
1111            },
1112            s2,
1113        );
1114        a.add_edge(s2, Some(LeafId(0)), Transition::Epsilon, s3);
1115        a.add_edge(s3, None, Transition::FirstMatchEnd { group: g }, s4);
1116        a.add_edge(s4, None, Transition::Match { actions: vec![] }, s4);
1117        a
1118    }
1119
1120    #[test]
1121    fn first_match_keeps_only_the_earliest_end() {
1122        let a = first_of_window();
1123        let tr = rows(&[&[false, true, true, true]]);
1124        assert_eq!(attempt(&a, 0, 4, &tr).unwrap().ends, BTreeSet::from([1]));
1125        // Without the markers all three would match: the guard alone is not
1126        // what narrows the result.
1127        let mut plain = a.clone();
1128        for st in &mut plain.states {
1129            for e in &mut st.edges {
1130                if matches!(
1131                    e.label,
1132                    Transition::FirstMatchBegin { .. } | Transition::FirstMatchEnd { .. }
1133                ) {
1134                    e.label = Transition::Epsilon;
1135                }
1136            }
1137        }
1138        assert_eq!(
1139            attempt(&plain, 0, 4, &tr).unwrap().ends,
1140            BTreeSet::from([1, 2]),
1141            "window is ticks 0..=2 and the guard holds on 1 and 2"
1142        );
1143    }
1144
1145    #[test]
1146    fn first_match_regions_beginning_on_different_ticks_are_independent() {
1147        // `##[0:1] first_match(##[0:2] l0)`: begin tick 0 and begin tick 1 each
1148        // keep their own earliest end.
1149        let mut a = Automaton::empty();
1150        let g = a.new_first_match_group();
1151        let s0 = a.start;
1152        let (s1, s2, s3, s4, s5) = (
1153            a.new_state(),
1154            a.new_state(),
1155            a.new_state(),
1156            a.new_state(),
1157            a.new_state(),
1158        );
1159        a.add_edge(
1160            s0,
1161            None,
1162            Transition::Delay {
1163                min: 0,
1164                max: Some(1),
1165            },
1166            s1,
1167        );
1168        a.add_edge(s1, None, Transition::FirstMatchBegin { group: g }, s2);
1169        a.add_edge(
1170            s2,
1171            None,
1172            Transition::Delay {
1173                min: 0,
1174                max: Some(2),
1175            },
1176            s3,
1177        );
1178        a.add_edge(s3, Some(LeafId(0)), Transition::Epsilon, s4);
1179        a.add_edge(s4, None, Transition::FirstMatchEnd { group: g }, s5);
1180        a.add_edge(s5, None, Transition::Match { actions: vec![] }, s5);
1181        // leaf true on ticks 1 and 3. Begin 0: window 0..=2, earliest 1.
1182        // Begin 1: window 1..=3, earliest 1. So only tick 1 -- tick 3 is
1183        // blocked for begin 1 by begin 1's OWN earlier end.
1184        let tr = rows(&[&[false, true, false, true, false]]);
1185        assert_eq!(attempt(&a, 0, 5, &tr).unwrap().ends, BTreeSet::from([1]));
1186        // The discriminating case for keying the table by BEGIN tick: leaf
1187        // true on ticks 0 and 1. Begin 0's earliest end is 0; begin 1's is 1,
1188        // which a table keyed by group alone would wrongly block (0 < 1).
1189        let tr = rows(&[&[true, true, false, false, false]]);
1190        assert_eq!(attempt(&a, 0, 5, &tr).unwrap().ends, BTreeSet::from([0, 1]));
1191        // leaf true on ticks 2 and 3 only: begin 0 -> earliest 2; begin 1 ->
1192        // earliest 2. Tick 3 is never a first match.
1193        let tr = rows(&[&[false, false, true, true, false]]);
1194        assert_eq!(attempt(&a, 0, 5, &tr).unwrap().ends, BTreeSet::from([2]));
1195        // leaf true on tick 3 only: begin 0's window (0..=2) misses it, begin
1196        // 1's window reaches it, and nothing earlier blocks it.
1197        let tr = rows(&[&[false, false, false, true, false]]);
1198        assert_eq!(attempt(&a, 0, 5, &tr).unwrap().ends, BTreeSet::from([3]));
1199    }
1200
1201    #[test]
1202    fn first_match_end_without_begin_is_a_named_error() {
1203        let mut a = Automaton::empty();
1204        let s0 = a.start;
1205        let s1 = a.new_state();
1206        a.add_edge(
1207            s0,
1208            None,
1209            Transition::FirstMatchEnd {
1210                group: FirstMatchId(7),
1211            },
1212            s1,
1213        );
1214        let tr = rows(&[]);
1215        assert_eq!(
1216            attempt(&a, 0, 2, &tr),
1217            Err(TraceError::EndWithoutBegin(FirstMatchId(7)))
1218        );
1219    }
1220
1221    #[test]
1222    fn nested_begin_of_an_open_group_is_a_named_error() {
1223        let mut a = Automaton::empty();
1224        let g = FirstMatchId(0);
1225        let s0 = a.start;
1226        let (s1, s2) = (a.new_state(), a.new_state());
1227        a.add_edge(s0, None, Transition::FirstMatchBegin { group: g }, s1);
1228        a.add_edge(s1, None, Transition::FirstMatchBegin { group: g }, s2);
1229        let tr = rows(&[]);
1230        assert_eq!(attempt(&a, 0, 2, &tr), Err(TraceError::BeginWhileOpen(g)));
1231    }
1232
1233    // -- T13: the local-variable frame ------------------------------------
1234
1235    /// `eval` for a table of per-leaf closures over `(tick, frame)`.
1236    type Eval<'a> = &'a dyn Fn(LeafId, u32, &[Option<i64>]) -> Result<i64, TraceError>;
1237
1238    /// `v0 = leaf0` (a 4-bit unsigned slot), then a guard `leaf1`, then a match.
1239    fn write_then_guard() -> Automaton {
1240        let mut a = Automaton::empty();
1241        let v0 = a.new_local_var(4, false);
1242        let s0 = a.start;
1243        let (s1, s2, s3) = (a.new_state(), a.new_state(), a.new_state());
1244        a.add_edge(
1245            s0,
1246            None,
1247            Transition::LocalVarWrite {
1248                var: v0,
1249                value: LeafId(0),
1250            },
1251            s1,
1252        );
1253        a.add_edge(
1254            s1,
1255            None,
1256            Transition::Delay {
1257                min: 1,
1258                max: Some(1),
1259            },
1260            s2,
1261        );
1262        a.add_edge(s2, Some(LeafId(1)), Transition::Epsilon, s3);
1263        a.add_edge(s3, None, Transition::Match { actions: vec![] }, s3);
1264        a
1265    }
1266
1267    #[test]
1268    fn a_local_write_is_cast_to_the_slot_type_and_later_leaves_read_it() {
1269        let a = write_then_guard();
1270        // leaf 0 is the constant 0x1f (does not fit 4 bits: stores 0xf); leaf 1
1271        // is `v0 == 15`.
1272        let ev: Eval = &|id, _t, frame| match id.0 {
1273            0 => Ok(0x1f),
1274            1 => Ok(i64::from(read_local(frame, LocalVarId(0))? == 15)),
1275            _ => unreachable!(),
1276        };
1277        let r = attempt_with_locals(&a, 0, 3, ev).unwrap();
1278        assert_eq!(r.ends(), BTreeSet::from([1]));
1279        assert_eq!(
1280            r.matches,
1281            BTreeSet::from([(1, vec![Some(0xf)])]),
1282            "the frame at the match carries the cast value"
1283        );
1284        // The same automaton with a guard that wants the UNCAST value fails.
1285        let ev: Eval = &|id, _t, frame| match id.0 {
1286            0 => Ok(0x1f),
1287            1 => Ok(i64::from(read_local(frame, LocalVarId(0))? == 0x1f)),
1288            _ => unreachable!(),
1289        };
1290        assert!(
1291            attempt_with_locals(&a, 0, 3, ev)
1292                .unwrap()
1293                .matches
1294                .is_empty()
1295        );
1296    }
1297
1298    #[test]
1299    fn reading_an_unassigned_slot_is_a_named_error() {
1300        let mut a = write_then_guard();
1301        // Drop the write: the guard now reads a slot nothing assigned.
1302        a.states[0].edges[0].label = Transition::Epsilon;
1303        let ev: Eval = &|id, _t, frame| match id.0 {
1304            1 => Ok(i64::from(read_local(frame, LocalVarId(0))? == 15)),
1305            _ => unreachable!(),
1306        };
1307        assert_eq!(
1308            attempt_with_locals(&a, 0, 3, ev),
1309            Err(TraceError::UnassignedRead(LocalVarId(0)))
1310        );
1311        let e = TraceError::UnassignedRead(LocalVarId(3));
1312        assert!(e.to_string().contains("unassigned local variable 3"), "{e}");
1313    }
1314
1315    /// A forking thread carries its own COPY of the frame (16.10 rule (c)): two
1316    /// branches that assign different values reach the match with one frame each.
1317    #[test]
1318    fn forked_threads_keep_independent_frames() {
1319        let mut a = Automaton::empty();
1320        let v0 = a.new_local_var(8, false);
1321        let s0 = a.start;
1322        let (l, r, end) = (a.new_state(), a.new_state(), a.new_state());
1323        a.add_edge(s0, None, Transition::Epsilon, l);
1324        a.add_edge(s0, None, Transition::Epsilon, r);
1325        for (from, leaf) in [(l, 0), (r, 1)] {
1326            a.add_edge(
1327                from,
1328                None,
1329                Transition::LocalVarWrite {
1330                    var: v0,
1331                    value: LeafId(leaf),
1332                },
1333                end,
1334            );
1335        }
1336        a.add_edge(end, None, Transition::Match { actions: vec![] }, end);
1337        let ev: Eval = &|id, _t, _f| Ok(10 + i64::from(id.0));
1338        let r = attempt_with_locals(&a, 0, 1, ev).unwrap();
1339        assert_eq!(
1340            r.matches,
1341            BTreeSet::from([(0, vec![Some(10)]), (0, vec![Some(11)])])
1342        );
1343        assert_eq!(r.ends(), BTreeSet::from([0]));
1344    }
1345
1346    /// `LocalVarCopy` casts to the destination's type and propagates
1347    /// "unassigned".
1348    #[test]
1349    fn a_local_copy_casts_to_the_destination_and_keeps_unassigned() {
1350        let mut a = Automaton::empty();
1351        let wide = a.new_local_var(8, true);
1352        let narrow = a.new_local_var(4, true);
1353        let never = a.new_local_var(8, true);
1354        let sink = a.new_local_var(8, true);
1355        let s0 = a.start;
1356        let (s1, s2, s3, s4) = (a.new_state(), a.new_state(), a.new_state(), a.new_state());
1357        a.add_edge(
1358            s0,
1359            None,
1360            Transition::LocalVarWrite {
1361                var: wide,
1362                value: LeafId(0),
1363            },
1364            s1,
1365        );
1366        a.add_edge(
1367            s1,
1368            None,
1369            Transition::LocalVarCopy {
1370                dst: narrow,
1371                src: wide,
1372            },
1373            s2,
1374        );
1375        // `sink` is assigned first, then overwritten with `never`'s "unassigned".
1376        a.add_edge(
1377            s2,
1378            None,
1379            Transition::LocalVarWrite {
1380                var: sink,
1381                value: LeafId(0),
1382            },
1383            s3,
1384        );
1385        a.add_edge(
1386            s3,
1387            None,
1388            Transition::LocalVarCopy {
1389                dst: sink,
1390                src: never,
1391            },
1392            s4,
1393        );
1394        a.add_edge(s4, None, Transition::Match { actions: vec![] }, s4);
1395        let ev: Eval = &|_, _, _| Ok(0x39);
1396        let r = attempt_with_locals(&a, 0, 1, ev).unwrap();
1397        // 0x39 is 57 in a signed 8-bit slot; truncated to 4 bits that is 0x9 = -7.
1398        assert_eq!(
1399            r.matches,
1400            BTreeSet::from([(0, vec![Some(0x39), Some(-7), None, None])])
1401        );
1402    }
1403
1404    /// The plain boolean entry point has no frame, so a local edge is refused.
1405    #[test]
1406    fn the_plain_attempt_refuses_local_edges() {
1407        let a = write_then_guard();
1408        let tr = rows(&[]);
1409        assert_eq!(
1410            attempt(&a, 0, 3, &tr),
1411            Err(TraceError::UnsupportedEdge("LocalVarWrite"))
1412        );
1413        let mut b = Automaton::empty();
1414        let (x, y) = (b.new_local_var(1, false), b.new_local_var(1, false));
1415        let (s0, s1) = (b.start, b.new_state());
1416        b.add_edge(s0, None, Transition::LocalVarCopy { dst: x, src: y }, s1);
1417        assert_eq!(
1418            attempt(&b, 0, 3, &tr),
1419            Err(TraceError::UnsupportedEdge("LocalVarCopy"))
1420        );
1421    }
1422
1423    // -- T14: match-item edges ---------------------------------------------
1424
1425    /// `v0 = leaf0` then `v0++` then `v0--` `n` times as given, then a match.
1426    fn write_then_steps(width: u32, signed: bool, deltas: &[i64]) -> Automaton {
1427        let mut a = Automaton::empty();
1428        let v0 = a.new_local_var(width, signed);
1429        let mut cur = a.start;
1430        let next = a.new_state();
1431        a.add_edge(
1432            cur,
1433            None,
1434            Transition::LocalVarWrite {
1435                var: v0,
1436                value: LeafId(0),
1437            },
1438            next,
1439        );
1440        cur = next;
1441        for d in deltas {
1442            let next = a.new_state();
1443            a.add_edge(
1444                cur,
1445                None,
1446                Transition::LocalVarStep { var: v0, delta: *d },
1447                next,
1448            );
1449            cur = next;
1450        }
1451        a.add_edge(cur, None, Transition::Match { actions: vec![] }, cur);
1452        a
1453    }
1454
1455    #[test]
1456    fn a_step_adds_its_delta_and_wraps_at_the_slot_width() {
1457        let ev: Eval = &|_, _, _| Ok(14);
1458        let a = write_then_steps(4, false, &[1, 1, 1]);
1459        // 14 -> 15 -> 0 -> 1 in a 4-bit unsigned slot.
1460        assert_eq!(
1461            attempt_with_locals(&a, 0, 1, ev).unwrap().matches,
1462            BTreeSet::from([(0, vec![Some(1)])])
1463        );
1464        let a = write_then_steps(4, true, &[1, 1]);
1465        // 14 is -2 in a signed 4-bit slot; stepping up gives -1 then 0.
1466        assert_eq!(
1467            attempt_with_locals(&a, 0, 1, ev).unwrap().matches,
1468            BTreeSet::from([(0, vec![Some(0)])])
1469        );
1470        let a = write_then_steps(8, false, &[-1, -1]);
1471        assert_eq!(
1472            attempt_with_locals(&a, 0, 1, ev).unwrap().matches,
1473            BTreeSet::from([(0, vec![Some(12)])])
1474        );
1475    }
1476
1477    #[test]
1478    fn a_step_of_an_unassigned_slot_is_a_named_error() {
1479        let mut a = write_then_steps(8, false, &[1]);
1480        a.states[0].edges[0].label = Transition::Epsilon;
1481        let ev: Eval = &|_, _, _| Ok(0);
1482        assert_eq!(
1483            attempt_with_locals(&a, 0, 1, ev),
1484            Err(TraceError::UnassignedRead(LocalVarId(0)))
1485        );
1486    }
1487
1488    /// A variable of 130 bits is written limb by limb (value limbs, then unknown
1489    /// limbs: leaf `i` is the i-th slot's word), stepped, and copied to a second
1490    /// one and to a narrow one: the whole variable moves, not just its head slot.
1491    #[test]
1492    fn a_wide_local_is_written_stepped_and_copied_as_a_whole() {
1493        let mut a = Automaton::empty();
1494        let w = a.new_local_var(130, false);
1495        let w2 = a.new_local_var(130, false);
1496        let n = a.new_local_var(8, false);
1497        assert_eq!(
1498            (w.0, w2.0, n.0, a.local_vars.len()),
1499            (0, 6, 12, 13),
1500            "three limbs of value and of unknown plane per wide variable"
1501        );
1502        let mut cur = a.start;
1503        for slot in 0..6u32 {
1504            let next = a.new_state();
1505            a.add_edge(
1506                cur,
1507                None,
1508                Transition::LocalVarWrite {
1509                    var: LocalVarId(slot),
1510                    value: LeafId(slot),
1511                },
1512                next,
1513            );
1514            cur = next;
1515        }
1516        for t in [
1517            Transition::LocalVarStep { var: w, delta: 1 },
1518            Transition::LocalVarCopy { dst: w2, src: w },
1519            Transition::LocalVarCopy { dst: n, src: w },
1520        ] {
1521            let next = a.new_state();
1522            a.add_edge(cur, None, t, next);
1523            cur = next;
1524        }
1525        a.add_edge(cur, None, Transition::Match { actions: vec![] }, cur);
1526        // value limbs [!0, 1, 3], unknown limbs [0, 0, 0]
1527        let words = [-1i64, 1, 3, 0, 0, 0];
1528        let ev: Eval = &|id, _t, _f| Ok(words[id.0 as usize]);
1529        let r = attempt_with_locals(&a, 0, 1, ev).unwrap();
1530        let limbs = [Some(0), Some(2), Some(3), Some(0), Some(0), Some(0)];
1531        let mut frame = limbs.to_vec();
1532        frame.extend(limbs);
1533        frame.push(Some(0));
1534        assert_eq!(
1535            r.matches,
1536            BTreeSet::from([(0, frame)]),
1537            "!0 + 1 carries into limb 1; the copies take every limb, the narrow one the low byte"
1538        );
1539    }
1540
1541    /// Stepping a wide variable with an unknown bit makes it all `x`.
1542    #[test]
1543    fn a_step_of_a_wide_local_with_an_unknown_bit_is_unknown() {
1544        let mut a = Automaton::empty();
1545        let w = a.new_local_var(70, false);
1546        let mut cur = a.start;
1547        for slot in 0..4u32 {
1548            let next = a.new_state();
1549            a.add_edge(
1550                cur,
1551                None,
1552                Transition::LocalVarWrite {
1553                    var: LocalVarId(slot),
1554                    value: LeafId(slot),
1555                },
1556                next,
1557            );
1558            cur = next;
1559        }
1560        let next = a.new_state();
1561        a.add_edge(
1562            cur,
1563            None,
1564            Transition::LocalVarStep { var: w, delta: 1 },
1565            next,
1566        );
1567        a.add_edge(next, None, Transition::Match { actions: vec![] }, next);
1568        let words = [5i64, 0, 1, 0];
1569        let ev: Eval = &|id, _t, _f| Ok(words[id.0 as usize]);
1570        let r = attempt_with_locals(&a, 0, 1, ev).unwrap();
1571        assert_eq!(
1572            r.matches,
1573            BTreeSet::from([(0, vec![Some(0), Some(0), Some(-1), Some(0x3f)])])
1574        );
1575    }
1576
1577    /// `(l0, call#0(l1, ref)) ##1 (l2, call#1()) `: both calls fire, in the
1578    /// order of the end points, with the sampled value of by-value arguments.
1579    fn two_calls() -> Automaton {
1580        let mut a = Automaton::empty();
1581        let s0 = a.start;
1582        let (s1, s2, s3, s4, s5) = (
1583            a.new_state(),
1584            a.new_state(),
1585            a.new_state(),
1586            a.new_state(),
1587            a.new_state(),
1588        );
1589        a.add_edge(s0, Some(LeafId(0)), Transition::Epsilon, s1);
1590        a.add_edge(
1591            s1,
1592            None,
1593            Transition::SubroutineCall {
1594                call: CallId(0),
1595                args: vec![CallArg::Value(LeafId(1)), CallArg::Ref, CallArg::Const],
1596            },
1597            s2,
1598        );
1599        a.add_edge(
1600            s2,
1601            None,
1602            Transition::Delay {
1603                min: 1,
1604                max: Some(1),
1605            },
1606            s3,
1607        );
1608        a.add_edge(s3, Some(LeafId(2)), Transition::Epsilon, s4);
1609        a.add_edge(
1610            s4,
1611            None,
1612            Transition::SubroutineCall {
1613                call: CallId(1),
1614                args: vec![],
1615            },
1616            s5,
1617        );
1618        a.add_edge(s5, None, Transition::Match { actions: vec![] }, s5);
1619        a
1620    }
1621
1622    #[test]
1623    fn a_call_is_recorded_with_its_sampled_arguments() {
1624        let a = two_calls();
1625        // leaf 1 is the tick number plus 100: the sampled value at the call.
1626        let ev: Eval = &|id, t, _| match id.0 {
1627            1 => Ok(100 + i64::from(t)),
1628            _ => Ok(1),
1629        };
1630        let r = attempt_with_calls(&a, 0, 3, ev).unwrap();
1631        assert_eq!(r.attempt.ends(), BTreeSet::from([1]));
1632        assert_eq!(
1633            r.calls,
1634            BTreeSet::from([
1635                CallEvent {
1636                    tick: 0,
1637                    call: CallId(0),
1638                    args: vec![Some(100), None, None]
1639                },
1640                CallEvent {
1641                    tick: 1,
1642                    call: CallId(1),
1643                    args: vec![]
1644                },
1645            ])
1646        );
1647    }
1648
1649    /// A call fires at its end point even when the sequence it is part of then
1650    /// fails (16.11): the second operand never holds, no match, yet call #0
1651    /// was still scheduled.
1652    #[test]
1653    fn a_call_fires_even_when_the_longer_sequence_fails() {
1654        let a = two_calls();
1655        let ev: Eval = &|id, _, _| match id.0 {
1656            2 => Ok(0),
1657            _ => Ok(1),
1658        };
1659        let r = attempt_with_calls(&a, 0, 3, ev).unwrap();
1660        assert!(r.attempt.matches.is_empty());
1661        assert_eq!(
1662            r.calls.iter().map(|c| c.call).collect::<Vec<_>>(),
1663            vec![CallId(0)]
1664        );
1665    }
1666
1667    /// Items run in list order: a call placed between two writes sees only the
1668    /// first write.
1669    #[test]
1670    fn calls_and_writes_run_in_list_order() {
1671        let mut a = Automaton::empty();
1672        let v0 = a.new_local_var(8, false);
1673        let s0 = a.start;
1674        let (s1, s2, s3, s4) = (a.new_state(), a.new_state(), a.new_state(), a.new_state());
1675        a.add_edge(
1676            s0,
1677            None,
1678            Transition::LocalVarWrite {
1679                var: v0,
1680                value: LeafId(0),
1681            },
1682            s1,
1683        );
1684        a.add_edge(
1685            s1,
1686            None,
1687            Transition::SubroutineCall {
1688                call: CallId(0),
1689                args: vec![CallArg::Value(LeafId(1))],
1690            },
1691            s2,
1692        );
1693        a.add_edge(s2, None, Transition::LocalVarStep { var: v0, delta: 5 }, s3);
1694        a.add_edge(
1695            s3,
1696            None,
1697            Transition::SubroutineCall {
1698                call: CallId(1),
1699                args: vec![CallArg::Value(LeafId(1))],
1700            },
1701            s4,
1702        );
1703        a.add_edge(s4, None, Transition::Match { actions: vec![] }, s4);
1704        // leaf 0 is 3; leaf 1 reads v0.
1705        let ev: Eval = &|id, _, frame| match id.0 {
1706            0 => Ok(3),
1707            _ => read_local(frame, LocalVarId(0)),
1708        };
1709        let r = attempt_with_calls(&a, 0, 1, ev).unwrap();
1710        let by_call: Vec<_> = r.calls.iter().map(|c| (c.call.0, c.args[0])).collect();
1711        assert_eq!(by_call, vec![(0, Some(3)), (1, Some(8))]);
1712        assert_eq!(r.attempt.matches, BTreeSet::from([(0, vec![Some(8)])]));
1713    }
1714
1715    #[test]
1716    fn the_plain_attempt_refuses_call_edges() {
1717        let a = two_calls();
1718        let tr = rows(&[&[true], &[true], &[true]]);
1719        assert_eq!(
1720            attempt(&a, 0, 3, &tr),
1721            Err(TraceError::UnsupportedEdge("SubroutineCall"))
1722        );
1723        let a = write_then_steps(4, false, &[1]);
1724        assert_eq!(
1725            attempt(&a, 0, 3, &tr),
1726            Err(TraceError::UnsupportedEdge("LocalVarWrite"))
1727        );
1728        let mut b = Automaton::empty();
1729        let v = b.new_local_var(1, false);
1730        let (s0, s1) = (b.start, b.new_state());
1731        b.add_edge(s0, None, Transition::LocalVarStep { var: v, delta: 1 }, s1);
1732        assert_eq!(
1733            attempt(&b, 0, 3, &tr),
1734            Err(TraceError::UnsupportedEdge("LocalVarStep"))
1735        );
1736    }
1737
1738    #[test]
1739    fn abort_edges_are_refused_not_ignored() {
1740        let mut a = Automaton::empty();
1741        let s0 = a.start;
1742        let s1 = a.new_state();
1743        a.add_edge(
1744            s0,
1745            None,
1746            Transition::Abort {
1747                leaf: LeafId(0),
1748                mode: AbortMode::Accept,
1749            },
1750            s1,
1751        );
1752        let tr = rows(&[]);
1753        assert_eq!(
1754            attempt(&a, 0, 2, &tr),
1755            Err(TraceError::UnsupportedEdge("Abort"))
1756        );
1757        // The locals-aware entry point has no abort semantics either.
1758        let ev: Eval = &|_, _, _| Ok(1);
1759        assert_eq!(
1760            attempt_with_locals(&a, 0, 2, ev),
1761            Err(TraceError::UnsupportedEdge("Abort"))
1762        );
1763    }
1764
1765    #[test]
1766    fn start_outside_the_trace_matches_nothing_and_admits_empty_passes_through() {
1767        let mut a = a_then_b();
1768        a.admits_empty = true;
1769        let tr = rows(&[&[true], &[true]]);
1770        let r = attempt(&a, 5, 3, &tr).unwrap();
1771        assert!(r.ends.is_empty());
1772        assert!(r.admits_empty);
1773    }
1774
1775    /// `guard(leaf) ; Match`: the one-tick sequence `leaf`.
1776    fn one_tick(leaf: u32) -> Automaton {
1777        let mut a = Automaton::empty();
1778        let s0 = a.start;
1779        let s1 = a.new_state();
1780        a.add_edge(s0, Some(LeafId(leaf)), Transition::Epsilon, s1);
1781        a.add_edge(s1, None, Transition::Match { actions: vec![] }, s1);
1782        a
1783    }
1784
1785    /// An automaton whose only thing is to test `sub`'s end point, then (if
1786    /// `then` is a leaf) require it, then match.
1787    fn tests_end_point(
1788        method: EndMethod,
1789        sub: Automaton,
1790        flow_out: Vec<(LocalVarId, LocalVarId)>,
1791        flag: Option<LocalVarId>,
1792        then: Option<LeafId>,
1793    ) -> Automaton {
1794        let mut a = Automaton::empty();
1795        a.local_vars = sub.local_vars.clone();
1796        let p = a.new_end_point(EndPoint {
1797            method,
1798            site: 0,
1799            automaton: sub,
1800            flow_out,
1801        });
1802        let s0 = a.start;
1803        let (s1, s2) = (a.new_state(), a.new_state());
1804        a.add_edge(s0, None, Transition::EndPoint { point: p, flag }, s1);
1805        a.add_edge(s1, then, Transition::Epsilon, s2);
1806        a.add_edge(s2, None, Transition::Match { actions: vec![] }, s2);
1807        a
1808    }
1809
1810    /// 16.9.11: the end point status is a property of the instance, not of the
1811    /// attempt that tests it -- `l0 ##1 l1` ends on tick 1 whichever tick the
1812    /// testing sequence starts on.
1813    #[test]
1814    fn triggered_holds_where_the_instance_ends_whatever_the_start() {
1815        let a = tests_end_point(EndMethod::Triggered, a_then_b(), vec![], None, None);
1816        let ev: Eval = &|id, t, _| {
1817            let rows: [[i64; 4]; 2] = [[1, 0, 1, 0], [0, 1, 0, 0]];
1818            Ok(rows[id.0 as usize][t as usize])
1819        };
1820        for start in 0..4 {
1821            let r = attempt_with_locals(&a, start, 4, ev).unwrap();
1822            let want: BTreeSet<u32> = if start == 1 {
1823                BTreeSet::from([1])
1824            } else {
1825                BTreeSet::new()
1826            };
1827            assert_eq!(r.ends(), want, "start {start}");
1828        }
1829    }
1830
1831    /// 16.9.11 / `zero_or_one_req`: a sequence that can match empty never makes
1832    /// `.triggered` true by its empty match, only by a nonempty one.
1833    #[test]
1834    fn an_empty_match_never_triggers() {
1835        let mut sub = one_tick(0);
1836        sub.admits_empty = true;
1837        let a = tests_end_point(EndMethod::Triggered, sub, vec![], None, None);
1838        let never: Eval = &|_, _, _| Ok(0);
1839        for start in 0..3 {
1840            assert!(
1841                attempt_with_locals(&a, start, 3, never)
1842                    .unwrap()
1843                    .matches
1844                    .is_empty(),
1845                "empty match fired the end point at start {start}"
1846            );
1847        }
1848        let once: Eval = &|_, t, _| Ok(i64::from(t == 1));
1849        assert_eq!(
1850            attempt_with_locals(&a, 1, 3, once).unwrap().ends(),
1851            BTreeSet::from([1])
1852        );
1853    }
1854
1855    /// The flag form (`!s.triggered`): the thread goes on either way and the
1856    /// status lands in the slot the leaf reads.
1857    #[test]
1858    fn the_flag_form_stores_the_status_and_never_blocks() {
1859        let mut sub = one_tick(0);
1860        let flag = sub.new_local_var(1, false);
1861        let a = tests_end_point(
1862            EndMethod::Triggered,
1863            sub,
1864            vec![],
1865            Some(flag),
1866            Some(LeafId(1)),
1867        );
1868        // sub matches on ticks 0 and 2 only; leaf 1 is `!flag`.
1869        let ev: Eval = &|id, t, frame| match id.0 {
1870            0 => Ok(i64::from(t == 0 || t == 2)),
1871            _ => Ok(i64::from(read_local(frame, LocalVarId(0))? == 0)),
1872        };
1873        let r = attempt_with_locals(&a, 0, 4, ev).unwrap();
1874        assert!(r.matches.is_empty(), "tick 0 is triggered, `!flag` fails");
1875        let r = attempt_with_locals(&a, 1, 4, ev).unwrap();
1876        assert_eq!(r.matches, BTreeSet::from([(1, vec![Some(0)])]));
1877        let r = attempt_with_locals(&a, 3, 4, ev).unwrap();
1878        assert_eq!(r.matches, BTreeSet::from([(3, vec![Some(0)])]));
1879        let r = attempt_with_locals(&a, 2, 4, ev).unwrap();
1880        assert!(r.matches.is_empty());
1881    }
1882
1883    /// 16.10: a local variable assigned in the instance flows out of the
1884    /// maximal application, one thread per distinct frame.
1885    #[test]
1886    fn a_maximal_application_forks_per_frame_and_flows_variables_out() {
1887        let mut sub = Automaton::empty();
1888        let dst = sub.new_local_var(8, false); // slot 0: the testing thread's
1889        let src = sub.new_local_var(8, false); // slot 1: the instance's own
1890        let s0 = sub.start;
1891        let (w1, w2, m) = (sub.new_state(), sub.new_state(), sub.new_state());
1892        sub.add_edge(s0, None, Transition::Epsilon, w1);
1893        sub.add_edge(s0, None, Transition::Epsilon, w2);
1894        sub.add_edge(
1895            w1,
1896            None,
1897            Transition::LocalVarWrite {
1898                var: src,
1899                value: LeafId(0),
1900            },
1901            m,
1902        );
1903        sub.add_edge(
1904            w2,
1905            None,
1906            Transition::LocalVarWrite {
1907                var: src,
1908                value: LeafId(1),
1909            },
1910            m,
1911        );
1912        sub.add_edge(m, None, Transition::Match { actions: vec![] }, m);
1913        let a = tests_end_point(EndMethod::Triggered, sub, vec![(src, dst)], None, None);
1914        let ev: Eval = &|id, t, _| Ok(10 * (i64::from(id.0) + 1) + i64::from(t));
1915        let r = attempt_with_locals(&a, 2, 3, ev).unwrap();
1916        assert_eq!(
1917            r.matches,
1918            BTreeSet::from([(2, vec![Some(12), None]), (2, vec![Some(22), None]),]),
1919            "two frames, two threads; the instance's own slot stays out of the thread"
1920        );
1921    }
1922
1923    /// 16.13.5: `.matched` is true at a testing tick when the instance matched
1924    /// since the previous testing tick, up to and including the tick's own time.
1925    #[test]
1926    fn matched_hands_off_across_clocks() {
1927        let a = tests_end_point(EndMethod::Matched, one_tick(0), vec![], None, None);
1928        let clocks = TraceClocks {
1929            dest: vec![10, 20, 30],
1930            source: vec![5, 12, 18, 25, 31],
1931        };
1932        // The source clock's leaf holds on source ticks 0, 1, 2 and 4.
1933        let ev: Eval = &|_, t, _| Ok(i64::from(matches!(t, 0..=2 | 4)));
1934        let ends = |start| {
1935            attempt_with_clocks(&a, start, &clocks, ev)
1936                .unwrap()
1937                .attempt
1938                .ends()
1939        };
1940        assert_eq!(ends(0), BTreeSet::from([0]), "t=5 <= 10");
1941        assert_eq!(ends(1), BTreeSet::from([1]), "t=12 and t=18 in (10, 20]");
1942        assert!(
1943            ends(2).is_empty(),
1944            "t=25 does not hold, t=31 is past tick 30"
1945        );
1946        // A match at the very time of the testing tick is seen by it.
1947        let at_tick = TraceClocks {
1948            dest: vec![10, 20],
1949            source: vec![10, 15],
1950        };
1951        let only_first: Eval = &|_, t, _| Ok(i64::from(t == 0));
1952        let r = attempt_with_clocks(&a, 0, &at_tick, only_first).unwrap();
1953        assert_eq!(r.attempt.ends(), BTreeSet::from([0]));
1954        // ...and is not repeated at the next one.
1955        let r = attempt_with_clocks(&a, 1, &at_tick, only_first).unwrap();
1956        assert!(r.attempt.matches.is_empty());
1957    }
1958
1959    #[test]
1960    fn matched_needs_a_clock_model_and_does_not_nest() {
1961        let a = tests_end_point(EndMethod::Matched, one_tick(0), vec![], None, None);
1962        let ev: Eval = &|_, _, _| Ok(1);
1963        // Without a per-domain clock model the instance runs over the run's own
1964        // ticks (one shared clock): the handoff is the tick itself.
1965        assert!(attempt_with_locals(&a, 0, 2, ev).is_ok());
1966        let clocks = TraceClocks {
1967            dest: vec![1, 2],
1968            source: vec![1, 2],
1969        };
1970        let inner = tests_end_point(EndMethod::Matched, one_tick(0), vec![], None, None);
1971        let outer = tests_end_point(EndMethod::Matched, inner, vec![], None, None);
1972        assert_eq!(
1973            attempt_with_clocks(&outer, 0, &clocks, ev),
1974            Err(TraceError::NestedMatched)
1975        );
1976        let bad = TraceClocks {
1977            dest: vec![2, 2],
1978            source: vec![1],
1979        };
1980        assert_eq!(
1981            attempt_with_clocks(&a, 0, &bad, ev),
1982            Err(TraceError::BadClocks)
1983        );
1984    }
1985
1986    /// An instance's own match-item calls are reported once it has been tested.
1987    #[test]
1988    fn calls_inside_an_instance_are_reported() {
1989        let a = tests_end_point(EndMethod::Triggered, two_calls(), vec![], None, None);
1990        let ev: Eval = &|_, _, _| Ok(1);
1991        let r = attempt_with_calls(&a, 1, 3, ev).unwrap();
1992        assert_eq!(r.attempt.ends(), BTreeSet::from([1]));
1993        assert!(r.calls.iter().any(|c| c.call == CallId(1)));
1994    }
1995
1996    #[test]
1997    fn the_plain_attempt_refuses_end_point_edges() {
1998        let a = tests_end_point(EndMethod::Triggered, one_tick(0), vec![], None, None);
1999        let tr = rows(&[&[true]]);
2000        assert_eq!(
2001            attempt(&a, 0, 1, &tr),
2002            Err(TraceError::UnsupportedEdge("EndPoint"))
2003        );
2004    }
2005
2006    #[test]
2007    fn display_names_the_group() {
2008        let e = TraceError::EndWithoutBegin(FirstMatchId(3));
2009        assert!(e.to_string().contains("FirstMatchEnd(3)"), "{e}");
2010        let e = TraceError::BeginWhileOpen(FirstMatchId(4));
2011        assert!(e.to_string().contains("FirstMatchBegin(4)"), "{e}");
2012        let e = TraceError::UnsupportedEdge("Abort");
2013        assert!(e.to_string().contains("Abort"), "{e}");
2014    }
2015
2016    #[test]
2017    fn tail_word_letters_flip_and_cut() {
2018        let w = TailWord::weak(2);
2019        assert_eq!(w.letter(1), None);
2020        assert_eq!(w.letter(2), Some(true));
2021        assert_eq!(w.letter(99), Some(true));
2022        assert_eq!(w.extent(), 2);
2023        let s = w.flip();
2024        assert_eq!(s, TailWord::strong(2));
2025        assert_eq!(s.letter(5), Some(false));
2026        // A `⊥`-tailed word cut at tick 4 keeps two `⊥` letters, then `⊤`.
2027        let cut = s.cut_then_top(4);
2028        assert_eq!(cut.letter(2), Some(false));
2029        assert_eq!(cut.letter(3), Some(false));
2030        assert_eq!(cut.letter(4), Some(true));
2031        assert_eq!(cut.extent(), 4);
2032        assert_eq!(cut.flip().letter(2), Some(true), "flip swaps mid letters");
2033        assert_eq!(cut.flip().letter(4), Some(false));
2034        // A cut inside the real samples drops everything after it.
2035        assert_eq!(s.cut_then_top(1), TailWord::weak(1));
2036        // Cutting a `⊤` tail adds no `⊥`.
2037        assert_eq!(w.cut_then_top(5), TailWord::weak(2));
2038    }
2039
2040    /// `l0 ##1 l1` over `a ⊤` tail: a `⊤` letter satisfies every guard, `⊥` none.
2041    #[test]
2042    fn attempt_word_guards_follow_the_tail_letter() {
2043        let a = a_then_b();
2044        let table: &[&[bool]] = &[&[true], &[false]];
2045        let eval = |l: LeafId, t: u32, _: &[Option<i64>]| -> Result<i64, TraceError> {
2046            Ok(i64::from(
2047                table[l.0 as usize]
2048                    .get(t as usize)
2049                    .copied()
2050                    .unwrap_or(false),
2051            ))
2052        };
2053        let run = |w: &TailWord| {
2054            attempt_word(&a, a.start, &[], 0, w, 6, &eval)
2055                .unwrap()
2056                .into_iter()
2057                .map(|(e, _)| e)
2058                .collect::<Vec<_>>()
2059        };
2060        // `a` at tick 0, then the `⊤` tail supplies `b` at tick 1.
2061        assert_eq!(run(&TailWord::weak(1)), vec![1]);
2062        // A `⊥` tail never does: even a constant-true leaf is not satisfied by it.
2063        assert_eq!(run(&TailWord::strong(1)), Vec::<u32>::new());
2064        // Starting past the real samples on a `⊤` tail matches immediately.
2065        let on_tail = attempt_word(&a, a.start, &[], 3, &TailWord::weak(1), 6, &eval).unwrap();
2066        assert_eq!(
2067            on_tail.into_iter().map(|(e, _)| e).collect::<Vec<_>>(),
2068            vec![4]
2069        );
2070    }
2071
2072    /// A call is scheduled where a thread REALLY is: one reached on a real tick
2073    /// is recorded with its sampled arguments, one reached on a `⊤` letter is
2074    /// not (16.11: the call is attached to an end point of a sampled sequence).
2075    #[test]
2076    fn attempt_word_calls_records_real_ticks_only() {
2077        let a = two_calls();
2078        let eval = |l: LeafId, t: u32, _: &[Option<i64>]| -> Result<i64, TraceError> {
2079            Ok(match l.0 {
2080                1 => 100 + i64::from(t),
2081                _ => 1,
2082            })
2083        };
2084        let run = |real: u32| {
2085            attempt_word_calls(
2086                &a,
2087                a.start,
2088                &[],
2089                0,
2090                &TailWord::weak(real),
2091                6,
2092                &eval,
2093                None,
2094                a.entry_clock,
2095            )
2096            .unwrap()
2097        };
2098        let (matches, calls) = run(1);
2099        assert_eq!(
2100            matches.into_iter().map(|(e, _)| e).collect::<Vec<_>>(),
2101            vec![1]
2102        );
2103        assert_eq!(
2104            calls.into_iter().collect::<Vec<_>>(),
2105            vec![CallEvent {
2106                tick: 0,
2107                call: CallId(0),
2108                args: vec![Some(100), None, None]
2109            }],
2110            "the second call would fire on tick 1, a `⊤` letter"
2111        );
2112        let (_, calls) = run(3);
2113        assert_eq!(
2114            calls.iter().map(|c| (c.tick, c.call.0)).collect::<Vec<_>>(),
2115            vec![(0, 0), (1, 1)]
2116        );
2117        // The set-returning wrapper still drops them: same matches either way.
2118        let plain = attempt_word(&a, a.start, &[], 0, &TailWord::weak(3), 6, &eval).unwrap();
2119        assert_eq!(plain, run(3).0);
2120    }
2121
2122    /// A start state other than `Automaton::start` and an initial frame are honoured.
2123    #[test]
2124    fn attempt_word_starts_at_the_given_entry_with_the_given_frame() {
2125        let mut a = Automaton::empty();
2126        let v = a.new_local_var(8, false);
2127        let entry = a.new_state();
2128        let end = a.new_state();
2129        a.add_edge(entry, None, Transition::Match { actions: vec![] }, entry);
2130        let _ = end;
2131        let eval = |_: LeafId, _: u32, _: &[Option<i64>]| Ok(1);
2132        let frame = vec![Some(7)];
2133        let got = attempt_word(&a, entry, &frame, 0, &TailWord::weak(1), 3, &eval).unwrap();
2134        assert_eq!(
2135            got.into_iter().collect::<Vec<_>>(),
2136            vec![(0, vec![Some(7)])]
2137        );
2138        assert_eq!(v, LocalVarId(0));
2139    }
2140
2141    // -- T28: multiclock timelines (16.13) ---------------------------------
2142
2143    const C0: ClockId = ClockId(0);
2144    const C1: ClockId = ClockId(1);
2145
2146    #[test]
2147    fn timeline_counts_strictly_subsequent_ticks_and_continues_into_the_tail() {
2148        // clock 0 ticks at 0, 2, 4; clock 1 at 1, 2; six real steps.
2149        let tl = Timeline::new(6, vec![vec![0, 2, 4], vec![1, 2]]).unwrap();
2150        assert_eq!(tl.real(), 6);
2151        assert!(tl.is_tick(C0, 2) && !tl.is_tick(C0, 3) && tl.is_tick(C1, 1));
2152        // `##1` from a tick of the OTHER clock is the nearest strictly later tick.
2153        assert_eq!(tl.kth_after(C1, 0, 1), 1);
2154        assert_eq!(
2155            tl.kth_after(C1, 1, 1),
2156            2,
2157            "strictly later: not tick 1 itself"
2158        );
2159        assert_eq!(
2160            tl.kth_after(C1, 2, 1),
2161            6,
2162            "list exhausted: the tail starts at `real`"
2163        );
2164        assert_eq!(tl.kth_after(C0, 0, 2), 4);
2165        assert_eq!(tl.kth_after(C0, 4, 3), 8, "every tail position is a tick");
2166        assert_eq!(
2167            tl.kth_after(C0, 9, 2),
2168            11,
2169            "past `real` every position ticks"
2170        );
2171        assert_eq!(tl.kth_after(C0, 3, 0), 3, "k = 0 is the position itself");
2172        // `next_tick` is at-or-after.
2173        assert_eq!(tl.next_tick(C0, 2), 2);
2174        assert_eq!(tl.next_tick(C0, 3), 4);
2175        assert_eq!(tl.next_tick(C0, 5), 6);
2176        assert_eq!(tl.next_tick(C1, 7), 7);
2177        // A clock with no list ticks everywhere.
2178        assert_eq!(tl.kth_after(ClockId(5), 1, 1), 2);
2179        assert_eq!(tl.next_tick(ClockId(5), 3), 3);
2180        let rows = Timeline::from_rows(&[&[true, false, true], &[false, true]]);
2181        assert_eq!(rows.real(), 3);
2182        assert!(rows.is_tick(C1, 1) && !rows.is_tick(C1, 0));
2183    }
2184
2185    #[test]
2186    fn a_malformed_timeline_is_a_named_error() {
2187        assert_eq!(
2188            Timeline::new(4, vec![vec![2, 2]]),
2189            Err(TraceError::BadTimeline)
2190        );
2191        assert_eq!(
2192            Timeline::new(4, vec![vec![1, 4]]),
2193            Err(TraceError::BadTimeline),
2194            "a tick must be a real step"
2195        );
2196        assert!(
2197            TraceError::BadTimeline
2198                .to_string()
2199                .contains("strictly increasing")
2200        );
2201    }
2202
2203    /// `l0 ##1 @(c1) l1` (or `##0` when `!strict`): guard l0 on clock 0, a
2204    /// `ClockSync` to clock 1, guard l1, match.
2205    fn cross_clock(strict: bool) -> Automaton {
2206        let mut a = Automaton::empty();
2207        let s0 = a.start;
2208        let (s1, s2, s3) = (a.new_state(), a.new_state(), a.new_state());
2209        a.add_edge(s0, Some(LeafId(0)), Transition::Epsilon, s1);
2210        a.add_edge(s1, None, Transition::ClockSync { to: C1, strict }, s2);
2211        a.add_edge(s2, Some(LeafId(1)), Transition::Epsilon, s3);
2212        a.add_edge(s3, None, Transition::Match { actions: vec![] }, s3);
2213        a
2214    }
2215
2216    /// Ends of one attempt of `a` from `start` over `rows` (leaf rows) on `tl`.
2217    fn ends_on(
2218        a: &Automaton,
2219        start: u32,
2220        tl: &Timeline,
2221        leaves: &'static [&'static [bool]],
2222    ) -> Vec<u32> {
2223        let ev = move |l: LeafId, t: u32, _: &[Option<i64>]| -> Result<i64, TraceError> {
2224            Ok(i64::from(
2225                leaves[l.0 as usize]
2226                    .get(t as usize)
2227                    .copied()
2228                    .unwrap_or(false),
2229            ))
2230        };
2231        attempt_word_in(
2232            a,
2233            a.start,
2234            &[],
2235            start,
2236            &TailWord::weak(tl.real()),
2237            tl.real(),
2238            &ev,
2239            Some(tl),
2240            C0,
2241        )
2242        .unwrap()
2243        .into_iter()
2244        .map(|(e, _)| e)
2245        .collect()
2246    }
2247
2248    /// 16.13.1: "the nearest strictly subsequent tick of the second clock".
2249    #[test]
2250    fn strict_clock_sync_waits_for_the_nearest_strictly_later_tick() {
2251        // Clock 0 ticks at 0 and 4; clock 1 at 0, 2, 3.
2252        let tl = Timeline::from_rows(&[
2253            &[true, false, false, false, true],
2254            &[true, false, true, true, false],
2255        ]);
2256        // l0 holds at 0; l1 holds at 0, 2, 3. `##1` skips the coincident tick 0
2257        // and lands on tick 2: only the l1 sample at 2 is read.
2258        let leaves: &'static [&'static [bool]] = &[
2259            &[true, false, false, false, false],
2260            &[true, false, false, true, false],
2261        ];
2262        assert!(
2263            ends_on(&cross_clock(true), 0, &tl, leaves).is_empty(),
2264            "tick 2 is the target and l1 is false there; tick 0 and 3 are not read"
2265        );
2266        let leaves: &'static [&'static [bool]] = &[
2267            &[true, false, false, false, false],
2268            &[true, false, true, true, false],
2269        ];
2270        assert_eq!(ends_on(&cross_clock(true), 0, &tl, leaves), vec![2]);
2271    }
2272
2273    /// 16.13.1: `##0` may overlap: "the nearest possibly overlapping tick".
2274    #[test]
2275    fn nonstrict_clock_sync_may_overlap_the_current_tick() {
2276        let tl = Timeline::from_rows(&[
2277            &[true, false, false, false, true],
2278            &[true, false, true, true, false],
2279        ]);
2280        let leaves: &'static [&'static [bool]] = &[
2281            &[true, false, false, false, false],
2282            &[true, false, false, false, false],
2283        ];
2284        assert_eq!(
2285            ends_on(&cross_clock(false), 0, &tl, leaves),
2286            vec![0],
2287            "tick 0 is a tick of both clocks: the match overlaps"
2288        );
2289        // Where the clocks do not coincide the sync moves forward to clock 1's
2290        // next tick, exactly as `##1` would.
2291        let tl = Timeline::from_rows(&[&[true, false, false, false], &[false, false, true, false]]);
2292        let leaves: &'static [&'static [bool]] =
2293            &[&[true, false, false, false], &[false, false, true, false]];
2294        assert_eq!(ends_on(&cross_clock(false), 0, &tl, leaves), vec![2]);
2295        assert_eq!(ends_on(&cross_clock(true), 0, &tl, leaves), vec![2]);
2296    }
2297
2298    /// An attempt starts at the first tick of its entry clock at or after `start`.
2299    #[test]
2300    fn an_attempt_aligns_its_start_to_the_entry_clock() {
2301        let a = a_then_b();
2302        let tl = Timeline::from_rows(&[&[false, false, true, false, true, false]]);
2303        let leaves: &'static [&'static [bool]] = &[
2304            &[false, false, true, false, false, false],
2305            &[false, false, false, false, true, false],
2306        ];
2307        // Started at step 0, the attempt waits for the first clock-0 tick (2), and
2308        // its `##1` is the NEXT tick of the clock (4), not step 3.
2309        assert_eq!(ends_on(&a, 0, &tl, leaves), vec![4]);
2310    }
2311
2312    /// A `Delay` after a sync counts ticks of the NEW clock.
2313    #[test]
2314    fn delay_counts_ticks_of_the_threads_current_clock() {
2315        // `l0 ##1 @(c1) l1 ##2 l2`
2316        let mut a = cross_clock(true);
2317        let m = ThreadStateId(3);
2318        let (d, g, e) = (a.new_state(), a.new_state(), a.new_state());
2319        a.states[m.0 as usize].edges.clear();
2320        a.add_edge(
2321            m,
2322            None,
2323            Transition::Delay {
2324                min: 2,
2325                max: Some(2),
2326            },
2327            d,
2328        );
2329        a.add_edge(d, Some(LeafId(2)), Transition::Epsilon, g);
2330        a.add_edge(g, None, Transition::Match { actions: vec![] }, g);
2331        let _ = e;
2332        // Clock 1 ticks at 1, 3, 4, 6; clock 0 at 0 only.
2333        let tl = Timeline::from_rows(&[
2334            &[true, false, false, false, false, false, false],
2335            &[false, true, false, true, true, false, true],
2336        ]);
2337        let leaves: &'static [&'static [bool]] = &[
2338            &[true, false, false, false, false, false, false],
2339            &[false, true, false, false, false, false, false],
2340            &[false, false, false, false, true, false, true],
2341        ];
2342        // l1 at the sync tick 1; `##2` is the second later tick of clock 1: 4.
2343        // l2 holds at 4 (and at 6, which is `##3`, not matched).
2344        assert_eq!(ends_on(&a, 0, &tl, leaves), vec![4]);
2345    }
2346
2347    /// With no timeline a sync is `##1` / `##0` over plain steps.
2348    #[test]
2349    fn without_a_timeline_a_clock_sync_is_plain_delay() {
2350        let table: &'static [&'static [bool]] = &[&[true, false, false], &[false, true, true]];
2351        let tr = rows(table);
2352        let strict = attempt(&cross_clock(true), 0, 3, &tr).unwrap();
2353        assert_eq!(strict.ends, BTreeSet::from([1]));
2354        let loose = attempt(&cross_clock(false), 0, 3, &tr).unwrap();
2355        assert!(
2356            loose.ends.is_empty(),
2357            "##0 reads l1 at tick 0, where it is false"
2358        );
2359    }
2360}