Skip to main content

vj_sim/sva/
property.rs

1//! SV-B (`docs/superpowers/plans/2026-10-03-sva.md` T19-T26): the property
2//! layer over T9-T18's sequence automata, and a reference evaluator for it --
3//! the oracle every property operator is tested against, as [`super::trace`] is
4//! for sequences. The runtime engine (T29) is a separate, incremental
5//! implementation of the same contract; this one evaluates the definitions of
6//! IEEE 1800-2023 Annex F.5 and F.7 as literally as it can, favouring being
7//! obviously right over being fast, and is never on a simulation path.
8//!
9//! A [`PropertyIr`] is a DAG of [`Prop`] nodes over ONE shared sequence
10//! [`Automaton`]: every sequence of the property (an antecedent, a `strong(...)`
11//! operand, a bare sequence property) is an entry state of that automaton and
12//! they share its local-variable slot table, so a frame carried out of an
13//! antecedent match is valid for the consequent's sequences. Like
14//! [`super::ir`] this module has no `sv-lang` dependency.
15//!
16//! # Semantics
17//!
18//! Satisfaction is over the infinite words of F.5.3: a finite trace of `len`
19//! samples completed by `⊤^ω` is the WEAK verdict (nothing has failed yet) and by
20//! `⊥^ω` the STRONG one (everything still owed has failed), see
21//! [`TailWord`]. [`Evaluator::verdict`] returns both. The relations are
22//! implemented as Annex F states them:
23//!
24//! - `strong(R)`: some NONEMPTY match of `R` from the start point.
25//!   `weak(R)`: for every `j`, the first `j` letters followed by `⊤^ω` strongly
26//!   satisfy `R` (no finite prefix witnesses the inability to match).
27//! - `not P` holds on `w` iff `P` does not hold on the dual word `w̄` (every
28//!   `⊤` letter `⊥` and vice versa): `not` switches strength.
29//! - `P1 or P2`, `P1 and P2`: the Boolean combinations. `P1 implies P2` is
30//!   `not P1 or P2` and `P1 iff P2` is `(P1 implies P2) and (P2 implies P1)`
31//!   (F.3.4.3, applied by the builder).
32//! - `R |-> P`: for every match of `R` from the start, `P` holds from its end
33//!   tick, with the frame the matching thread ended with. `R |=> P` is
34//!   `R ##1 1 |-> P`.
35//! - `nexttime`, `always`, `eventually`, `until`: positions are `w^{i..}`;
36//!   `s_nexttime p` is `not nexttime not p`, which on an infinite word is
37//!   `nexttime p` again (nexttime is self-dual there), and likewise for the other
38//!   `s_` operators of a bounded range -- the strong and weak forms differ only
39//!   in the verdict they are judged at, so the evaluator keeps one rule for each
40//!   pair and the builder keeps the strength for its legality checks.
41//! - `accept_on(b) P`: `P` holds and no letter satisfies `b`, or for some `i`,
42//!   `w_i ⊨ b` and `w^{0,i-1}⊤^ω ⊨ P` (F.5.6.1). `reject_on(b) P` is
43//!   `not accept_on(b) not P`.
44//! - Recursive properties (F.7): `w ⊨ π` iff `w ⊨ π[k]` for every `k`, where
45//!   `π[0]` is `1'b1` and `π[k+1]` unfolds each recursive instance once. See
46//!   [`Evaluator::recursion_limit`] for the bound `k` is run to.
47use std::cell::{Cell, RefCell};
48use std::collections::{BTreeSet, HashMap};
49use std::rc::Rc;
50
51use super::incremental::{Accel, automaton_span};
52use super::ir::{AbortMode, Automaton, ClockId, LeafId, LocalVarId, ThreadStateId, Transition};
53use super::trace::{CallEvent, Frame, Matches, TailWord, Timeline, TraceError, attempt_word_calls};
54
55/// Index of a [`Prop`] in [`PropertyIr::props`].
56#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
57pub struct PropId(pub u32);
58
59/// Index of a [`PropDecl`] in [`PropertyIr::decls`].
60#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
61pub struct DeclId(pub u32);
62
63/// One sequence of the property: its entry state in the shared automaton and
64/// whether it admits the empty match (16.9.2.1), which is a flag of the
65/// sequence, not a path of the automaton.
66///
67/// `clock` is the clock the sequence STARTS on and `end_clock` the one it ends on
68/// (16.13: they differ for a multiclocked sequence, `a ##1 @(c1) b`). The
69/// builder stamps both from the clock flow; a single-clock property has
70/// `ClockId(0)` in both.
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub struct SeqRef {
73    pub entry: ThreadStateId,
74    pub admits_empty: bool,
75    pub clock: ClockId,
76    pub end_clock: ClockId,
77}
78
79impl SeqRef {
80    /// A single-clocked sequence on the assertion's clock (`ClockId(0)`).
81    pub fn single(entry: ThreadStateId, admits_empty: bool) -> Self {
82        SeqRef {
83            entry,
84            admits_empty,
85            clock: ClockId(0),
86            end_clock: ClockId(0),
87        }
88    }
89}
90
91/// The `strong`/`weak` operator written on a sequence property (16.12.2).
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub enum Strength {
94    Strong,
95    Weak,
96}
97
98/// One arm of a `case` property: the labels that select it and its property.
99#[derive(Clone, Debug, PartialEq, Eq)]
100pub struct CaseArm {
101    pub labels: Vec<LeafId>,
102    pub body: PropId,
103}
104
105/// One property operator (16.12). Operands are [`PropId`]s of nodes built before
106/// this one, so the table is a DAG in topological order (a shared operand, as in
107/// the `iff` expansion, is one node referenced twice).
108#[derive(Clone, Debug, PartialEq, Eq)]
109pub enum Prop {
110    /// A sequence used as a property (16.12.2). `strength` is the explicit
111    /// `strong`/`weak` operator, or `None` for a bare sequence, which is weak
112    /// under `assert`/`assume` and strong under `cover` (the evaluator's
113    /// `implicit_strong`).
114    Seq {
115        seq: SeqRef,
116        strength: Option<Strength>,
117    },
118    /// `not p` (16.12.3).
119    Not(PropId),
120    /// `p1 and p2` (16.12.5).
121    And(PropId, PropId),
122    /// `p1 or p2` (16.12.4).
123    Or(PropId, PropId),
124    /// `if (cond) then [else els]` (16.12.6): `cond` is a Boolean leaf, and an
125    /// absent `else` is true.
126    If {
127        cond: LeafId,
128        then: PropId,
129        els: Option<PropId>,
130    },
131    /// `case (selector) labels: body ... [default: body] endcase` (16.12.16): the
132    /// first arm one of whose labels matches the selector is taken; no arm and no
133    /// `default` is vacuously true.
134    Case {
135        selector: LeafId,
136        arms: Vec<CaseArm>,
137        default: Option<PropId>,
138    },
139    /// `ante |-> cons` (`overlapped`) or `ante |=> cons` (16.12.7).
140    Implication {
141        ante: SeqRef,
142        cons: PropId,
143        overlapped: bool,
144    },
145    /// `nexttime [n] p` / `s_nexttime [n] p` (16.12.10).
146    Nexttime { p: PropId, n: u32, strong: bool },
147    /// `always [lo:hi] p` / `s_always [lo:hi] p` (16.12.11): `hi == None` is `$`.
148    Always {
149        p: PropId,
150        lo: u32,
151        hi: Option<u32>,
152        strong: bool,
153    },
154    /// `eventually [lo:hi] p` / `s_eventually [lo:hi] p` (16.12.13).
155    Eventually {
156        p: PropId,
157        lo: u32,
158        hi: Option<u32>,
159        strong: bool,
160    },
161    /// `p until q` / `p s_until q` (16.12.12); `until_with` is built as
162    /// `p until (p and q)`.
163    Until { p: PropId, q: PropId, strong: bool },
164    /// `accept_on`/`reject_on`/`sync_accept_on`/`sync_reject_on (cond) p`
165    /// (16.12.14).
166    Abort {
167        cond: LeafId,
168        p: PropId,
169        mode: AbortMode,
170    },
171    /// Writes local variables at the start of `body`'s evaluation, in order, each
172    /// value evaluated in the frame the earlier writes left (a property's local
173    /// formals and declared locals with initialisers, 16.12.19).
174    Let {
175        writes: Vec<(LocalVarId, LeafId)>,
176        body: PropId,
177    },
178    /// `@(clock) p` (16.13.2): `p` runs on `clock`, whatever clock the context
179    /// had. Every operand of `p` that counts time (`nexttime`, `always`, `until`,
180    /// a sequence's delays) counts ticks of `clock`.
181    Clocked { clock: ClockId, p: PropId },
182    /// An instance of a recursive property (16.12.17): evaluates `decl`'s body in
183    /// the caller's frame with the listed slots rebound to the arguments, every
184    /// one evaluated in the CALLER's frame first. Then `inits` (the instance's
185    /// declared locals with initialisers, in declaration order) are written: each
186    /// is evaluated in the caller's frame as the earlier `inits` left it, so that
187    /// it reads the formals as the instance's actuals (which is what slang
188    /// substitutes into an initialiser) and the declared locals it follows.
189    Call {
190        decl: DeclId,
191        args: Vec<(LocalVarId, LeafId)>,
192        inits: Vec<(LocalVarId, LeafId)>,
193    },
194}
195
196/// A recursive property's body, entered through [`Prop::Call`].
197#[derive(Clone, Debug, PartialEq, Eq)]
198pub struct PropDecl {
199    pub name: String,
200    pub body: PropId,
201}
202
203/// A built property: the shared sequence automaton, the operator DAG and the
204/// recursive declarations.
205#[derive(Clone, Debug, PartialEq, Eq)]
206pub struct PropertyIr {
207    pub automaton: Automaton,
208    pub props: Vec<Prop>,
209    pub decls: Vec<PropDecl>,
210    pub root: PropId,
211    /// The `disable iff (cond)` of the property spec (16.12, T27): `root` is the
212    /// property under it. `None` when the spec has no explicit clause. Only a
213    /// top-level clause exists -- the builder rejects a nested one -- and the
214    /// leaf is evaluated with CURRENT values (see [`Evaluator::attempt`]).
215    pub disable: Option<LeafId>,
216    /// Nodes whose 16.14.8 nonvacuity rule is not the one their shape says,
217    /// because the builder expressed an operator through others (see
218    /// [`NvRule`]). Empty for a hand-built property that has none.
219    pub nv_rules: Vec<(PropId, NvRule)>,
220}
221
222/// An operator built from others whose nonvacuity rule (16.14.8) differs from
223/// the derived shape's.
224#[derive(Clone, Copy, Debug, PartialEq, Eq)]
225pub enum NvRule {
226    /// The [`Prop::Or`] of `not p or q` standing for `p implies q` (rule z): `p`
227    /// must be true and nonvacuous, and `q` nonvacuous.
228    Implies,
229    /// The [`Prop::Until`] of `p until (p and q)` standing for `p until_with q`
230    /// (rules x, y): only `p` can make the attempt nonvacuous.
231    UntilWith,
232}
233
234impl PropertyIr {
235    /// The special nonvacuity rule of node `id`, if it has one.
236    pub fn nv_rule(&self, id: PropId) -> Option<NvRule> {
237        self.nv_rules
238            .iter()
239            .find(|(p, _)| *p == id)
240            .map(|(_, r)| *r)
241    }
242
243    /// Borrows `id`'s node (an id this IR's builder minted).
244    pub fn prop(&self, id: PropId) -> &Prop {
245        &self.props[id.0 as usize]
246    }
247
248    /// Whether the property contains a recursive instance ([`Prop::Call`]).
249    pub fn is_recursive(&self) -> bool {
250        self.props.iter().any(|p| matches!(p, Prop::Call { .. }))
251    }
252}
253
254/// What a property's leaves are on a trace.
255pub trait PropEnv {
256    /// The value of `leaf` at `tick` with the thread's local-variable `frame`; a
257    /// guard holds when nonzero. Called for real samples only.
258    fn value(&self, leaf: LeafId, tick: u32, frame: &[Option<i64>]) -> Result<i64, TraceError>;
259
260    /// Whether `label` selects the `case` selector at `tick`. The environment
261    /// owns the comparison: 16.12.16 uses the case equality of 12.5 (`===`), under
262    /// which an `x`/`z` selector matches no label that is not itself that value.
263    fn case_match(
264        &self,
265        selector: LeafId,
266        label: LeafId,
267        tick: u32,
268        frame: &[Option<i64>],
269    ) -> Result<bool, TraceError>;
270
271    /// The sampled value of the abort condition `leaf` at a time step STRICTLY
272    /// BETWEEN tick `after` and tick `after + 1` (16.12.14: `accept_on` and
273    /// `reject_on` are "evaluated at the granularity of the simulation time step",
274    /// unlike the `sync_` forms). A trace that has no such steps -- the default --
275    /// has none true.
276    fn between(&self, _leaf: LeafId, _after: u32) -> Result<bool, TraceError> {
277        Ok(false)
278    }
279
280    /// The tick lists of a multiclock property's clocks over the trace's
281    /// positions (16.13), or `None` -- the default -- when every position is a
282    /// tick of every clock, which is what a single-clocked property wants. With
283    /// a timeline the positions are the trace's global steps and `value` is read
284    /// at them; `between` is not consulted.
285    fn timeline(&self) -> Option<&Timeline> {
286        None
287    }
288
289    /// Local variables an attempt starts with already assigned: the values a
290    /// procedural concurrent assertion captured when it was reached (16.14.6).
291    fn initial_frame(&self) -> Vec<(LocalVarId, i64)> {
292        Vec::new()
293    }
294}
295
296/// The result of checking a property over a finite trace (F.5.3.2).
297#[derive(Clone, Copy, Debug, PartialEq, Eq)]
298pub enum Verdict {
299    /// `w ⊭- A`: the weak verdict fails -- the property has definitely failed.
300    Fails,
301    /// `w ⊨- A` but `w ⊭+ A`: nothing has failed but something is still owed
302    /// ("holds, but does not hold strongly", the evaluation is pending).
303    Holds,
304    /// `w ⊨+ A`: holds strongly.
305    HoldsStrongly,
306}
307
308/// What an evaluation attempt of a property spec came to (16.12 `disable iff`).
309#[derive(Clone, Copy, Debug, PartialEq, Eq)]
310pub enum Disposition {
311    /// The property failed (before any disabling).
312    Fails,
313    /// Nothing has failed and nothing is decided: the attempt is pending.
314    Pending,
315    /// The property holds strongly: it passed.
316    Passes,
317    /// The disable condition was true at some time step of the attempt while the
318    /// property was still undecided: "a disabled evaluation of a property does not
319    /// result in success or failure" (16.12).
320    Disabled,
321}
322
323impl From<Verdict> for Disposition {
324    fn from(v: Verdict) -> Self {
325        match v {
326            Verdict::Fails => Disposition::Fails,
327            Verdict::Holds => Disposition::Pending,
328            Verdict::HoldsStrongly => Disposition::Passes,
329        }
330    }
331}
332
333/// The evaluator: a property, the trace it is checked on and which strength a bare
334/// sequence takes.
335pub struct Evaluator<'a> {
336    ir: &'a PropertyIr,
337    env: &'a dyn PropEnv,
338    implicit_strong: bool,
339    /// Whether the trace is a prefix of a run that goes on (the live engine), see
340    /// [`Evaluator::open_future`].
341    open_future: bool,
342    /// An upper bound on how many ticks any simple path of the sequence automaton
343    /// can span, see [`Evaluator::horizon`].
344    span: u32,
345    /// The result of every [`Prop::Call`] evaluated so far, by what it depends on:
346    /// the declaration, the start tick, the word, the local-variable frame the
347    /// body starts with and the call depth (which decides where the unfolding is
348    /// cut off). A recursive property reaches the same configuration along many
349    /// paths (every match of an antecedent such as `x[->1]`, every abort point of
350    /// an `accept_on` re-runs the property after it), so without this the number
351    /// of evaluations grows exponentially with the depth of the unfolding.
352    calls: RefCell<HashMap<CallKey, bool>>,
353    /// The environment's [`Timeline`] cut to a word's real length, by that length
354    /// (see [`Self::timeline_for`]).
355    timelines: RefCell<HashMap<u32, Rc<Timeline>>>,
356    /// Set while [`Self::calls`] collects: every sequence search then unions the
357    /// subroutine calls its threads scheduled (16.11) into [`Self::call_sink`],
358    /// and the operators that would stop at the first failing operand
359    /// (`and`, `or`, `|->`) evaluate all of them, so the calls are those of every
360    /// thread the property starts, not of the ones a verdict happened to need.
361    exhaustive: Cell<bool>,
362    /// Set while [`Evaluator::verdict`] decides "holds strongly": a weak sequence
363    /// on the strong word then holds only once it has matched, see `weak_seq`.
364    pending_until_match: Cell<bool>,
365    call_sink: RefCell<Option<BTreeSet<CallEvent>>>,
366    /// The live engine's incremental sequence evaluator, see
367    /// [`Self::with_accel`]; `None` is the reference semantics, spelled out.
368    accel: Option<&'a RefCell<Accel>>,
369}
370
371/// What the settled ticks of an `until` say, see [`Evaluator::skip_settled_until`].
372enum Settled {
373    Verdict(bool),
374    Resume(u32),
375}
376
377/// What a [`Prop::Call`]'s result depends on, see the `calls` field of
378/// [`Evaluator`].
379type CallKey = (DeclId, u32, TailWord, Frame, u32, ClockId);
380
381impl<'a> Evaluator<'a> {
382    /// `implicit_strong` is whether a bare sequence property is strong (a `cover
383    /// property` directive) or weak (`assert`/`assume`), 16.12.2.
384    pub fn new(ir: &'a PropertyIr, env: &'a dyn PropEnv, implicit_strong: bool) -> Self {
385        Self::new_with_span(ir, env, implicit_strong, automaton_span(&ir.automaton))
386    }
387
388    /// [`Self::new`] with the automaton's [`automaton_span`] already known (an
389    /// engine builds one evaluator per evaluation).
390    pub fn new_with_span(
391        ir: &'a PropertyIr,
392        env: &'a dyn PropEnv,
393        implicit_strong: bool,
394        span: u32,
395    ) -> Self {
396        Evaluator {
397            ir,
398            env,
399            implicit_strong,
400            open_future: false,
401            span,
402            calls: RefCell::new(HashMap::new()),
403            timelines: RefCell::new(HashMap::new()),
404            exhaustive: Cell::new(false),
405            pending_until_match: Cell::new(false),
406            call_sink: RefCell::new(None),
407            accel: None,
408        }
409    }
410
411    /// Answers the sequence searches of words with no explicit letters (`strong`
412    /// and `weak` verdict words, which is what a live engine asks) from `accel`'s
413    /// incremental state instead of replaying them. The accelerator declines
414    /// what it does not model, and then the reference code below runs; the
415    /// results are the reference's, which the engine's differential mode and
416    /// this module's exhaustive test check.
417    pub fn with_accel(mut self, accel: Option<&'a RefCell<Accel>>) -> Self {
418        self.accel = accel;
419        self
420    }
421
422    /// Reads the trace as a PREFIX of a run that is not over, which is what a live
423    /// simulation has: the weak and strong verdicts then bracket every possible
424    /// continuation, so `Fails` and `HoldsStrongly` are final and `Holds` means
425    /// "undecided". Annex F's strong word `w⊥^ω` is the end of the run, where an
426    /// antecedent that still needs a letter has no match and the implication is
427    /// vacuously true (`a |=> b` holds strongly when `a` is the last letter). That is
428    /// the right verdict for a run that ended and the wrong one for a decision
429    /// taken in the middle of a run, so here an antecedent is matched on the DUAL
430    /// word (`⊤` letters in the strong word, `⊥` in the weak one): it matches as
431    /// eagerly as any continuation could make it in the strong word, as rarely in
432    /// the weak one, and the consequent is judged on the word itself.
433    pub fn open_future(mut self) -> Self {
434        self.open_future = true;
435        self
436    }
437
438    /// The clock the property starts on: the root context clock.
439    fn root_clock(&self) -> ClockId {
440        self.ir.automaton.entry_clock
441    }
442
443    /// The environment's timeline as `word` sees it: a position at or past
444    /// `word.real` is a `⊤`/`⊥` letter, which is a tick of EVERY clock (see
445    /// [`Timeline`]), so a word cut shorter than the trace needs the tick lists cut
446    /// with it. `None` when the property is single-clocked.
447    fn timeline_for(&self, word: &TailWord) -> Result<Option<Rc<Timeline>>, TraceError> {
448        let Some(full) = self.env.timeline() else {
449            return Ok(None);
450        };
451        if let Some(t) = self.timelines.borrow().get(&word.real) {
452            return Ok(Some(Rc::clone(t)));
453        }
454        let cut = Rc::new(full.truncated(word.real));
455        self.timelines
456            .borrow_mut()
457            .insert(word.real, Rc::clone(&cut));
458        Ok(Some(cut))
459    }
460
461    /// The `k`-th tick of `dom` strictly after `from` on `word` (`k == 0`: `from`).
462    fn after(&self, word: &TailWord, dom: ClockId, from: u32, k: u32) -> Result<u32, TraceError> {
463        Ok(match self.timeline_for(word)? {
464            Some(t) => t.kth_after(dom, from, k),
465            None => from + k,
466        })
467    }
468
469    /// The clock the property `id` starts on when that is evident from its own
470    /// operators (a clocked sequence, `@(c) p`, `not`, an `and`/`or` whose
471    /// operands agree); `fallback` otherwise.
472    fn lead_clock(&self, id: PropId, fallback: ClockId) -> ClockId {
473        match self.ir.prop(id) {
474            Prop::Seq { seq, .. } => seq.clock,
475            Prop::Clocked { clock, .. } => *clock,
476            Prop::Not(p) => self.lead_clock(*p, fallback),
477            Prop::Let { body, .. } => self.lead_clock(*body, fallback),
478            Prop::And(a, b) | Prop::Or(a, b) => {
479                let (x, y) = (self.lead_clock(*a, fallback), self.lead_clock(*b, fallback));
480                if x == y { x } else { fallback }
481            }
482            _ => fallback,
483        }
484    }
485
486    /// `pos` moved to the first tick of `dom` at or after it, folded into
487    /// `extent` (every position from there on is the same letter).
488    fn align(&self, word: &TailWord, dom: ClockId, pos: u32) -> Result<u32, TraceError> {
489        let at = match self.timeline_for(word)? {
490            Some(t) => t.next_tick(dom, pos),
491            None => pos,
492        };
493        Ok(at.min(word.extent()))
494    }
495
496    /// The verdict of an evaluation attempt started at tick `start` of a trace of
497    /// `len` samples.
498    pub fn verdict(&self, len: u32, start: u32) -> Result<Verdict, TraceError> {
499        let weak = self.holds(&TailWord::weak(len), start)?;
500        if !weak {
501            return Ok(Verdict::Fails);
502        }
503        self.pending_until_match.set(true);
504        let strong = self.holds(&TailWord::strong(len), start);
505        self.pending_until_match.set(false);
506        Ok(if strong? {
507            Verdict::HoldsStrongly
508        } else {
509            Verdict::Holds
510        })
511    }
512
513    /// [`Self::verdict`] for the whole property spec, `disable iff` included
514    /// (16.12): the attempt started at tick `start` of a trace of `len` samples.
515    ///
516    /// The disable condition is "true at anytime between the start of the attempt
517    /// in the Observed region, inclusive, and the end of the evaluation attempt,
518    /// inclusive". [`PropEnv::value`] of the disable leaf at tick `t` says whether
519    /// it was true in the Observed region of tick `t`, and [`PropEnv::between`]
520    /// whether it was true at some time step strictly between `t` and `t + 1`; both
521    /// are CURRENT values (16.12: "not sampled"). Let `p` be the first such moment
522    /// at or after the start; the attempt is judged on the letters before it -- the
523    /// ones that came before the disabling, `w^{0,i-1}` -- as the Annex F neutral
524    /// satisfaction of a top-level `disable iff` has it: failed if the weak completion of that prefix fails
525    /// (the failure was decided before the disabling), passed if its strong
526    /// completion holds, and DISABLED when it is neither (still pending).
527    ///
528    /// A condition true when the attempt starts disables it outright: Annex F
529    /// excludes letter 0 from `0 < i`, but 16.12's prose is explicit that the window
530    /// starts at the attempt's start, inclusive, and that is the clause that decides
531    /// what an evaluation attempt is. A decided verdict is the same on every longer
532    /// prefix (it does not depend on the continuation), so a disabling AFTER the
533    /// property was decided changes nothing.
534    pub fn attempt(&self, len: u32, start: u32) -> Result<Disposition, TraceError> {
535        let Some(prefix) = self.disabling_prefix(len, start)? else {
536            return Ok(self.verdict(len, start)?.into());
537        };
538        if prefix == start {
539            return Ok(Disposition::Disabled);
540        }
541        Ok(match self.verdict(prefix, start)? {
542            Verdict::Holds => Disposition::Disabled,
543            decided => decided.into(),
544        })
545    }
546
547    /// The number of letters (counted from tick 0) that precede the first moment at
548    /// or after `start` the disable condition is true, see [`Self::attempt`]; `None`
549    /// when there is no `disable iff` or it is never true on the `len` ticks.
550    pub fn disabling_prefix(&self, len: u32, start: u32) -> Result<Option<u32>, TraceError> {
551        let Some(cond) = self.ir.disable else {
552            return Ok(None);
553        };
554        // The attempt's own captured values (16.14.6.1) are what a condition over
555        // them reads; any other condition ignores the frame.
556        let frame = self.initial_frame();
557        for t in start..len {
558            if self.env.value(cond, t, &frame)? != 0 {
559                return Ok(Some(t));
560            }
561            if self.env.between(cond, t)? {
562                return Ok(Some(t + 1));
563            }
564        }
565        Ok(None)
566    }
567
568    /// The local-variable frame an attempt starts with: all unassigned, except
569    /// the slots the environment pre-assigns ([`PropEnv::initial_frame`]).
570    fn initial_frame(&self) -> Frame {
571        let mut frame = vec![None; self.ir.automaton.local_vars.len()];
572        for (var, value) in self.env.initial_frame() {
573            frame[var.0 as usize] = Some(value);
574        }
575        frame
576    }
577
578    /// Whether the property holds on `word` from tick `start` (an evaluation
579    /// attempt's start point), local variables unassigned save those the
580    /// environment pre-assigns.
581    pub fn holds(&self, word: &TailWord, start: u32) -> Result<bool, TraceError> {
582        // What the environment says may differ from one call to the next.
583        self.calls.borrow_mut().clear();
584        let frame = self.initial_frame();
585        self.sat(
586            self.ir.root,
587            word,
588            start,
589            &frame,
590            0,
591            self.root_clock(),
592            false,
593        )
594    }
595
596    /// Every nonempty match of the root sequence of a `cover sequence` (16.14.3)
597    /// attempt started at tick `start`, over the `len` real samples of the trace,
598    /// as `(end tick, the local-variable frame it matched with)`. Several matches
599    /// may end on one tick (with different frames, or the same frame reached along
600    /// different paths is one match): 16.14.3 counts every one. A match that would
601    /// need a tick past the trace is not listed -- it is reported when that tick
602    /// arrives. `NotASequence` for a root that is not a bare sequence.
603    pub fn root_sequence_matches(&self, len: u32, start: u32) -> Result<Matches, TraceError> {
604        let Prop::Seq { seq, .. } = self.ir.prop(self.ir.root) else {
605            return Err(TraceError::NotASequence);
606        };
607        self.calls.borrow_mut().clear();
608        let frame = self.initial_frame();
609        self.matches(*seq, &TailWord::strong(len), start, &frame)
610    }
611
612    /// How deep a [`Prop::Call`] chain is followed (`k` of F.7's `π[k]`); an
613    /// instance past it is `π[0]`, `1'b1`.
614    ///
615    /// Every recursive instance occurs after a positive time advance (16.12.17
616    /// restriction 3), so a chain of calls advances the start tick by at least one
617    /// each step, and past the trace (`extent`) every start tick is equivalent.
618    /// What can still differ between two calls is which recursive declaration
619    /// they enter and the local-variable arguments they pass. For a property with
620    /// `c` recursive declarations and no argument that grows, the configuration
621    /// `(declaration, tick clamped to extent)` repeats within `c * (extent + 1)`
622    /// calls, after which the unfolding is periodic and `π[k]` no longer changes
623    /// with `k` -- the limit below doubles that and adds slack. A recursion whose
624    /// local arguments grow forever (a counter) has no such fixpoint; for it the
625    /// verdict is that of this `k`, and the tests pin that `k` and `2k` agree on
626    /// every property of the LRM's recursive examples.
627    pub fn recursion_limit(&self, word: &TailWord) -> u32 {
628        2 * (self.ir.decls.len() as u32).max(1) * (word.extent() + 2) + 4
629    }
630
631    /// The tick the sequence search runs to for a start tick `pos` on `word`: past
632    /// the last tick that is not the constant tail, plus the longest a simple
633    /// path of the automaton can span, so every distinct way a match can end has
634    /// been tried.
635    fn horizon(&self, word: &TailWord, pos: u32) -> u32 {
636        word.extent().max(pos) + self.span + 1
637    }
638
639    /// Every nonempty match of `seq` started at `pos` with `frame`.
640    fn matches(
641        &self,
642        seq: SeqRef,
643        word: &TailWord,
644        pos: u32,
645        frame: &Frame,
646    ) -> Result<Matches, TraceError> {
647        if let Some(cell) = self.accel
648            && word.mid.is_empty()
649            && let Ok(mut accel) = cell.try_borrow_mut()
650            && let Some(found) = accel.matches(
651                &self.ir.automaton,
652                self.env,
653                seq.entry,
654                word.real,
655                word.tail_top,
656                pos,
657                frame,
658            )
659        {
660            return Ok(found);
661        }
662        let eval = |leaf: LeafId, tick: u32, f: &[Option<i64>]| self.env.value(leaf, tick, f);
663        let timeline = self.timeline_for(word)?;
664        let (found, calls) = attempt_word_calls(
665            &self.ir.automaton,
666            seq.entry,
667            frame,
668            pos,
669            word,
670            self.horizon(word, pos),
671            &eval,
672            timeline.as_deref(),
673            seq.clock,
674        )?;
675        if let Some(sink) = self.call_sink.borrow_mut().as_mut() {
676            sink.extend(calls);
677        }
678        Ok(found.into_iter().collect())
679    }
680
681    /// Every subroutine call (16.11) a thread of the evaluation attempt started at
682    /// tick `start` scheduled on the `len` samples of the trace, ascending by
683    /// `(tick, call, args)`: "all subroutine calls attached to a sequence are
684    /// executed at every end point of the sequence", whether or not the property
685    /// those sequences are part of later holds.
686    ///
687    /// The attempt is judged on the letters before its `disable iff` (16.12): a
688    /// call after the disabling is not the attempt's. The set depends only on
689    /// the samples up to each call's own tick (a thread reaches a call edge by
690    /// the letters before it), so an extension of the trace only ever ADDS
691    /// calls -- which is what lets the engine report each once.
692    ///
693    /// Both completions of the word are run: the verdict calls the second only
694    /// when the first holds, and the calls must not depend on the verdict.
695    ///
696    /// The calls are those the sequence searches find: they are exhaustive over
697    /// `and`/`or`/`|->` operands (see [`Self::exhaustive`]), and a property that
698    /// stops evaluating at a decision (`always`, `until`, ...) lists the calls of
699    /// the evaluations that ran.
700    pub fn scheduled_calls(&self, len: u32, start: u32) -> Result<Vec<CallEvent>, TraceError> {
701        let eff = self.disabling_prefix(len, start)?.unwrap_or(len);
702        *self.call_sink.borrow_mut() = Some(BTreeSet::new());
703        self.exhaustive.set(true);
704        let ran = self
705            .holds(&TailWord::weak(eff), start)
706            .and_then(|_| self.holds(&TailWord::strong(eff), start));
707        self.exhaustive.set(false);
708        let sink = self.call_sink.borrow_mut().take().unwrap_or_default();
709        ran?;
710        Ok(sink.into_iter().collect())
711    }
712
713    /// Whether a multiclock `cover sequence` attempt (16.13.1, 16.14.3) started at
714    /// position `start` of a trace of `len` positions has reported every match it
715    /// ever will: no thread of the root sequence can still be waiting for a
716    /// position past the trace.
717    ///
718    /// A single-clock attempt is retired by [`Automaton::max_span`], a count of
719    /// clock ticks. With several clocks the positions are the union of the clocks'
720    /// ticks, and how many positions a `##N` or a clock change takes depends on the
721    /// clock it waits for, so this follows the automaton on the timeline instead:
722    /// the latest position each state is reachable at, taking every `##[m:n]` to its
723    /// `n`-th tick and every clock change to its next tick of the new clock, guards
724    /// ignored (so it only ever over-estimates). A step that needs a tick not in the
725    /// trace yet, or an unbounded delay, leaves the attempt alive. `false` without
726    /// a timeline (a single clock: use `max_span`).
727    pub fn sequence_exhausted(&self, len: u32, start: u32) -> Result<bool, TraceError> {
728        let Prop::Seq { seq, .. } = self.ir.prop(self.ir.root) else {
729            return Err(TraceError::NotASequence);
730        };
731        let Some(tl) = self.env.timeline() else {
732            return Ok(false);
733        };
734        let au = &self.ir.automaton;
735        let first = tl.next_tick(seq.clock, start);
736        let mut best: HashMap<(u32, ClockId), u32> = HashMap::new();
737        let mut work = vec![((seq.entry.0, seq.clock), first)];
738        while let Some(((state, dom), pos)) = work.pop() {
739            if pos >= len {
740                return Ok(false);
741            }
742            if best.get(&(state, dom)).is_some_and(|&b| b >= pos) {
743                continue;
744            }
745            best.insert((state, dom), pos);
746            for edge in &au.state(ThreadStateId(state)).edges {
747                let target = edge.target.0;
748                match &edge.label {
749                    Transition::Delay { min, max } => {
750                        let Some(max) = max else {
751                            return Ok(false);
752                        };
753                        work.push(((target, dom), tl.kth_after(dom, pos, (*max).max(*min))));
754                    }
755                    Transition::ClockSync { to, strict } => {
756                        let at = if *strict {
757                            tl.kth_after(*to, pos, 1)
758                        } else {
759                            tl.next_tick(*to, pos)
760                        };
761                        work.push(((target, *to), at));
762                    }
763                    Transition::Match { .. } => {}
764                    Transition::Epsilon
765                    | Transition::Abort { .. }
766                    | Transition::LocalVarWrite { .. }
767                    | Transition::LocalVarCopy { .. }
768                    | Transition::FirstMatchBegin { .. }
769                    | Transition::FirstMatchEnd { .. }
770                    | Transition::LocalVarStep { .. }
771                    | Transition::SubroutineCall { .. }
772                    | Transition::EndPoint { .. } => work.push(((target, dom), pos)),
773                }
774            }
775        }
776        Ok(true)
777    }
778
779    /// The calls (16.11) of the threads of `cover sequence`'s root sequence
780    /// started at `start` (see [`Self::root_sequence_matches`]), over the `len`
781    /// real samples, before the attempt's `disable iff` (16.12).
782    pub fn sequence_calls(&self, len: u32, start: u32) -> Result<Vec<CallEvent>, TraceError> {
783        let eff = self.disabling_prefix(len, start)?.unwrap_or(len);
784        *self.call_sink.borrow_mut() = Some(BTreeSet::new());
785        let ran = self.root_sequence_matches(eff, start);
786        let sink = self.call_sink.borrow_mut().take().unwrap_or_default();
787        ran?;
788        Ok(sink.into_iter().collect())
789    }
790
791    /// A Boolean leaf at `tick`: a real sample's value, or the letter.
792    fn boolean(
793        &self,
794        leaf: LeafId,
795        word: &TailWord,
796        tick: u32,
797        frame: &Frame,
798    ) -> Result<bool, TraceError> {
799        match word.letter(tick) {
800            Some(top) => Ok(top),
801            None => Ok(self.env.value(leaf, tick, frame)? != 0),
802        }
803    }
804
805    /// `strong(seq)` from `pos`.
806    fn strong_seq(
807        &self,
808        seq: SeqRef,
809        word: &TailWord,
810        pos: u32,
811        frame: &Frame,
812    ) -> Result<bool, TraceError> {
813        if let Some(cell) = self.accel
814            && word.mid.is_empty()
815            && let Ok(mut accel) = cell.try_borrow_mut()
816            && let Some(found) = accel.strong(
817                &self.ir.automaton,
818                self.env,
819                seq.entry,
820                word.real,
821                word.tail_top,
822                pos,
823                frame,
824            )
825        {
826            return Ok(found);
827        }
828        Ok(!self.matches(seq, word, pos, frame)?.is_empty())
829    }
830
831    /// `weak(seq)` from `pos`: for every number of letters `n` past the start
832    /// point, `w^{pos..n-1}⊤^ω` strongly satisfies `seq` (F.5.3.1).
833    ///
834    /// On a `⊤` tail a cut past the real and explicit letters is the word itself,
835    /// so the cuts to try end there; on a `⊥` tail each longer cut adds a `⊥`
836    /// letter before the `⊤`s, and a cut longer than the longest path a match can
837    /// span from `pos` behaves like that one.
838    fn weak_seq(
839        &self,
840        seq: SeqRef,
841        word: &TailWord,
842        pos: u32,
843        frame: &Frame,
844    ) -> Result<bool, TraceError> {
845        // When deciding "holds strongly" (a PASS: `Evaluator::verdict`), a weak
846        // sequence on the strong word (`⊥` tail, no explicit letters) holds exactly
847        // when it has matched. Annex F's cuts are lenient
848        // on an unbounded range: `a ##[1:$] b` over `[a]⊥^ω` is matched by every
849        // cut `[a]⊥^k⊤^ω` (the `⊤` satisfies `b`), so it would hold strongly --
850        // the attempt would PASS at its first tick, before `b` is ever true. The
851        // LRM's own example (Table 40-3: `aundf: assert (on ##[1:$]
852        // off)` "will not pass or fail") says such an attempt stays pending until
853        // the sequence matches; a bounded sequence cannot tell the two apart (a
854        // match needs real letters, the `⊥`s kill every Boolean), so this only
855        // changes unbounded ranges, repetitions and goto/nonconsecutive forms.
856        if self.pending_until_match.get() && !word.tail_top && word.mid.is_empty() {
857            return self.strong_seq(seq, word, pos, frame);
858        }
859        if let Some(cell) = self.accel
860            && word.mid.is_empty()
861            && let Ok(mut accel) = cell.try_borrow_mut()
862            && let Some(found) = accel.weak(
863                &self.ir.automaton,
864                self.env,
865                seq.entry,
866                word.real,
867                word.tail_top,
868                pos,
869                frame,
870            )
871        {
872            return Ok(found);
873        }
874        let ext = word.extent();
875        let last = if word.tail_top {
876            ext.max(pos + 1)
877        } else {
878            ext.max(pos) + self.span + 1
879        };
880        for n in pos + 1..=last {
881            let cut = word.cut_then_top(n);
882            if !self.strong_seq(seq, &cut, pos, frame)? {
883                return Ok(false);
884            }
885        }
886        Ok(true)
887    }
888
889    /// The position `prop` stands on when asked at `pos`, and the pending
890    /// synchronization it hands on (see [`Self::sat`]'s `strict`).
891    ///
892    /// The pass-through operators hand the position on untouched; every other one
893    /// stands on a tick of the clock it starts on. A position past `extent` is the
894    /// one at it: every later tick carries the same letter.
895    fn stand(
896        &self,
897        prop: &Prop,
898        word: &TailWord,
899        pos: u32,
900        dom: ClockId,
901        strict: bool,
902    ) -> Result<(u32, bool), TraceError> {
903        let through = pass_through(prop);
904        let pos = if through {
905            pos.min(word.extent())
906        } else {
907            let lead = match prop {
908                Prop::Seq { seq, .. } => seq.clock,
909                Prop::Implication { ante, .. } => ante.clock,
910                // 16.13.7: a declaration assignment is performed at the earliest
911                // tick of the body's leading clock, not of the context clock.
912                Prop::Let { body, .. } => self.lead_clock(*body, dom),
913                _ => dom,
914            };
915            if strict {
916                self.after(word, lead, pos, 1)?.min(word.extent())
917            } else {
918                self.align(word, lead, pos)?
919            }
920        };
921        // Only a pass-through operator hands the pending synchronization on.
922        Ok((pos, strict && through))
923    }
924
925    /// The local-variable frame a [`Prop::Call`] body starts with: the caller's
926    /// frame with the formals' slots rebound (every actual is evaluated in the
927    /// caller's frame, before any slot is overwritten -- an actual may read the
928    /// slot its own formal is about to take, `fibonacci1(b, a+b, ...)`).
929    fn call_frame(
930        &self,
931        args: &[(LocalVarId, LeafId)],
932        inits: &[(LocalVarId, LeafId)],
933        word: &TailWord,
934        pos: u32,
935        frame: &Frame,
936    ) -> Result<Frame, TraceError> {
937        let mut f: Frame = frame.clone();
938        for (var, leaf) in args {
939            let raw = if word.letter(pos).is_some() {
940                0
941            } else {
942                self.env.value(*leaf, pos, frame)?
943            };
944            f[var.0 as usize] = Some(self.ir.automaton.local_var(*var).cast(raw));
945        }
946        let mut seen: Frame = frame.clone();
947        for (var, leaf) in inits {
948            let raw = if word.letter(pos).is_some() {
949                0
950            } else {
951                self.env.value(*leaf, pos, &seen)?
952            };
953            let v = Some(self.ir.automaton.local_var(*var).cast(raw));
954            f[var.0 as usize] = v;
955            seen[var.0 as usize] = v;
956        }
957        Ok(f)
958    }
959
960    /// `sat(prop, w, pos, frame)`: does `w^{pos..}` satisfy `prop` with the
961    /// local-variable `frame` as input context. `dom` is the clock the context is
962    /// running on (16.13.2): an evaluation point is a tick of it, so `pos` is moved
963    /// to the first tick of `dom` at or after it, and every operator that counts
964    /// time counts ticks of `dom`. A single-clocked property has every position a
965    /// tick and this is the identity.
966    ///
967    /// `strict` is the pending synchronization of a nonoverlapped implication's
968    /// consequent (16.13.2): "time is advanced from the end point of the match to
969    /// the nearest STRICTLY future occurrence" of the clock the consequent starts
970    /// on. It is carried down to wherever that clock is known -- through `not`,
971    /// `and`, `or` and `@(c)`, which add no time of their own, so that each branch
972    /// of `s0 |=> (@(c1) s1) and (@(c2) s2)` synchronizes to its own clock -- and
973    /// consumed by the first operator that stands on a position.
974    #[allow(clippy::too_many_arguments)]
975    fn sat(
976        &self,
977        id: PropId,
978        word: &TailWord,
979        pos: u32,
980        frame: &Frame,
981        depth: u32,
982        dom: ClockId,
983        strict: bool,
984    ) -> Result<bool, TraceError> {
985        let prop = self.ir.prop(id);
986        let (pos, strict) = self.stand(prop, word, pos, dom, strict)?;
987        match prop {
988            Prop::Seq { seq, strength } => {
989                let strong = match strength {
990                    Some(Strength::Strong) => true,
991                    Some(Strength::Weak) => false,
992                    None => self.implicit_strong,
993                };
994                if strong {
995                    self.strong_seq(*seq, word, pos, frame)
996                } else {
997                    self.weak_seq(*seq, word, pos, frame)
998                }
999            }
1000            Prop::Not(p) => Ok(!self.sat(*p, &word.flip(), pos, frame, depth, dom, strict)?),
1001            Prop::And(a, b) => {
1002                let left = self.sat(*a, word, pos, frame, depth, dom, strict)?;
1003                if !left && !self.exhaustive.get() {
1004                    return Ok(false);
1005                }
1006                let right = self.sat(*b, word, pos, frame, depth, dom, strict)?;
1007                Ok(left && right)
1008            }
1009            Prop::Or(a, b) => {
1010                let left = self.sat(*a, word, pos, frame, depth, dom, strict)?;
1011                if left && !self.exhaustive.get() {
1012                    return Ok(true);
1013                }
1014                let right = self.sat(*b, word, pos, frame, depth, dom, strict)?;
1015                Ok(left || right)
1016            }
1017            Prop::If { cond, then, els } => {
1018                if self.boolean(*cond, word, pos, frame)? {
1019                    self.sat(*then, word, pos, frame, depth, dom, false)
1020                } else if let Some(e) = els {
1021                    self.sat(*e, word, pos, frame, depth, dom, false)
1022                } else {
1023                    Ok(true)
1024                }
1025            }
1026            Prop::Case {
1027                selector,
1028                arms,
1029                default,
1030            } => {
1031                for arm in arms {
1032                    if self.arm_selected(*selector, arm, word, pos, frame)? {
1033                        return self.sat(arm.body, word, pos, frame, depth, dom, false);
1034                    }
1035                }
1036                match default {
1037                    Some(d) => self.sat(*d, word, pos, frame, depth, dom, false),
1038                    None => Ok(true),
1039                }
1040            }
1041            Prop::Implication {
1042                ante,
1043                cons,
1044                overlapped,
1045            } => {
1046                // A consequent with a clock of its own is a `Clocked` node (or a
1047                // sequence stamped with one) and moves itself.
1048                //
1049                // `|->` evaluates the consequent at the end point, and the
1050                // consequent's own clock "awaits its nearest tick", at or after
1051                // it. `|=>` advances from the end point to the nearest STRICTLY
1052                // future tick of the clock the consequent starts on (16.13.2, the
1053                // `strict` flag of `sat`); with one clock that is the next tick,
1054                // `ante ##1 1` of F.3.4. Annex F's rewrite, read literally for a
1055                // multiclocked `|=>`, advances one tick of the ANTECEDENT's clock
1056                // and only then awaits the consequent clock: the two differ
1057                // exactly when a tick of the consequent's clock falls between the
1058                // end point and the next tick of the antecedent's. The prose of
1059                // 16.13.2, being specific to this construct, is what is
1060                // implemented.
1061                // A consequent that carries no clock of its own runs on the clock
1062                // of the context the implication stands in (`dom`), not on the one
1063                // the antecedent ended on: Annex F.5.1 rewrites `r |-> p` under `c`
1064                // to `T_s(r, c) |-> T_p(p, c)`, and 16.13.3's `@(c) w ##1 (x ##1
1065                // @(d) y) |=> z` clocks `z` at `c`.
1066                let on = ante.end_clock;
1067                let dual;
1068                let aw = if self.open_future {
1069                    dual = word.flip();
1070                    &dual
1071                } else {
1072                    word
1073                };
1074                // A failing consequent decides the implication; collecting calls
1075                // goes on, as the consequent's threads (16.11) are evaluated too.
1076                let mut holds = true;
1077                for (end, f) in self.matches(*ante, aw, pos, frame)? {
1078                    if !*overlapped {
1079                        // The `1` of `ante ##1 1` is a Boolean on the next tick of
1080                        // the antecedent's clock, which no `⊥` letter satisfies
1081                        // (F.5.3). A consequent that starts on another clock is
1082                        // synchronized to that clock's next tick instead (16.13.2),
1083                        // so it is that tick that must exist: the verdict of a
1084                        // failing consequent is then reported at ITS tick, not at
1085                        // the antecedent clock's next one.
1086                        let next = self.after(aw, self.lead_clock(*cons, on), end, 1)?;
1087                        if aw.letter(next) == Some(false) {
1088                            continue;
1089                        }
1090                    }
1091                    if !self.sat(*cons, word, end, &f, depth, dom, !*overlapped)? {
1092                        if !self.exhaustive.get() {
1093                            return Ok(false);
1094                        }
1095                        holds = false;
1096                    }
1097                }
1098                // An empty match of the antecedent triggers `|=>` at the start
1099                // point itself (16.12.7): `(empty ##1 1)` is `1` on that tick.
1100                if !overlapped
1101                    && ante.admits_empty
1102                    && aw.letter(pos) != Some(false)
1103                    && !self.sat(*cons, word, pos, frame, depth, dom, false)?
1104                {
1105                    holds = false;
1106                }
1107                Ok(holds)
1108            }
1109            Prop::Nexttime { p, n, .. } => {
1110                let to = self.after(word, dom, pos, *n)?;
1111                self.sat(*p, word, to, frame, depth, dom, false)
1112            }
1113            Prop::Always { p, lo, hi, .. } => {
1114                if let Some(found) =
1115                    self.scan_range(*p, word, pos, *lo, *hi, frame, depth, dom, false)
1116                {
1117                    return found.map(|failed| !failed);
1118                }
1119                for t in self.positions(word, dom, pos, *lo, *hi)? {
1120                    if !self.sat(*p, word, t, frame, depth, dom, false)? {
1121                        return Ok(false);
1122                    }
1123                }
1124                Ok(true)
1125            }
1126            Prop::Eventually { p, lo, hi, .. } => {
1127                if let Some(found) =
1128                    self.scan_range(*p, word, pos, *lo, *hi, frame, depth, dom, true)
1129                {
1130                    return found;
1131                }
1132                for t in self.positions(word, dom, pos, *lo, *hi)? {
1133                    if self.sat(*p, word, t, frame, depth, dom, false)? {
1134                        return Ok(true);
1135                    }
1136                }
1137                Ok(false)
1138            }
1139            Prop::Until { p, q, strong } => {
1140                // `∃j: q at j, p at every i < j` or `p at every i`; a start point
1141                // past `extent` is `extent`, so the scan ends there and, having
1142                // passed every `p`, a weak `until` holds forever on. The `i` and
1143                // `j` are ticks of `dom`.
1144                let mut t = pos;
1145                if let Some(decided) = self.skip_settled_until(*p, *q, word, pos, frame, depth, dom)
1146                {
1147                    match decided {
1148                        Settled::Verdict(v) => return Ok(v),
1149                        Settled::Resume(from) => t = from,
1150                    }
1151                }
1152                loop {
1153                    if self.sat(*q, word, t, frame, depth, dom, false)? {
1154                        return Ok(true);
1155                    }
1156                    if !self.sat(*p, word, t, frame, depth, dom, false)? {
1157                        return Ok(false);
1158                    }
1159                    if t >= word.extent() {
1160                        break;
1161                    }
1162                    t = self.align(word, dom, self.after(word, dom, t, 1)?)?;
1163                }
1164                Ok(!strong)
1165            }
1166            Prop::Clocked { clock, p } => self.sat(*p, word, pos, frame, depth, *clock, strict),
1167            Prop::Abort { cond, p, mode } => {
1168                self.abort(*cond, *p, *mode, word, pos, frame, depth, dom)
1169            }
1170            Prop::Let { writes, body } => {
1171                let mut f = frame.clone();
1172                for (var, leaf) in writes {
1173                    let raw = if word.letter(pos).is_some() {
1174                        0
1175                    } else {
1176                        self.env.value(*leaf, pos, &f)?
1177                    };
1178                    f[var.0 as usize] = Some(self.ir.automaton.local_var(*var).cast(raw));
1179                }
1180                self.sat(*body, word, pos, &f, depth, dom, false)
1181            }
1182            Prop::Call { decl, args, inits } => {
1183                if depth >= self.recursion_limit(word) {
1184                    // `π[0]` is the Boolean `1'b1`: true on every letter but `⊥`.
1185                    return Ok(word.letter(pos) != Some(false));
1186                }
1187                // The body runs in the caller's frame with the formals' slots
1188                // rebound: every actual is evaluated in the caller's frame, before
1189                // any slot is overwritten (an actual may read the slot its own
1190                // formal is about to take, `fibonacci1(b, a+b, ...)`).
1191                let f = self.call_frame(args, inits, word, pos, frame)?;
1192                let key = (*decl, pos, word.clone(), f, depth, dom);
1193                if let Some(&known) = self.calls.borrow().get(&key) {
1194                    return Ok(known);
1195                }
1196                let body = self.ir.decls[decl.0 as usize].body;
1197                let holds = self.sat(body, word, pos, &key.3, depth + 1, dom, false)?;
1198                self.calls.borrow_mut().insert(key, holds);
1199                Ok(holds)
1200            }
1201        }
1202    }
1203
1204    /// Whether the evaluation attempt started at tick `start` of a trace of `len`
1205    /// samples is NONVACUOUS (16.14.8); together with a passing verdict it is a
1206    /// nonvacuous success.
1207    ///
1208    /// The attempt is judged on the letters it saw: those before the first moment
1209    /// the property spec's `disable iff` is true (rule ag: the condition holds in
1210    /// no time step of the attempt), all of them when it never is. The rules
1211    /// (a)-(af) are syntax-directed and live in [`Self::nv`], one arm per
1212    /// operator, next to [`Self::sat`]'s arm for the same operator.
1213    pub fn nonvacuous(&self, len: u32, start: u32) -> Result<bool, TraceError> {
1214        let end = self.disabling_prefix(len, start)?.unwrap_or(len);
1215        self.calls.borrow_mut().clear();
1216        let frame = self.initial_frame();
1217        self.nv(
1218            self.ir.root,
1219            &TailWord::strong(end),
1220            start,
1221            &frame,
1222            0,
1223            self.root_clock(),
1224            false,
1225        )
1226    }
1227
1228    /// 16.14.8: is the evaluation attempt of `id` from `pos` nonvacuous? The
1229    /// arguments are [`Self::sat`]'s. A "clock event" of the rules is a real tick
1230    /// of `dom` of the word, never one of its `⊤`/`⊥` letters.
1231    #[allow(clippy::too_many_arguments)]
1232    fn nv(
1233        &self,
1234        id: PropId,
1235        word: &TailWord,
1236        pos: u32,
1237        frame: &Frame,
1238        depth: u32,
1239        dom: ClockId,
1240        strict: bool,
1241    ) -> Result<bool, TraceError> {
1242        let prop = self.ir.prop(id);
1243        let (pos, strict) = self.stand(prop, word, pos, dom, strict)?;
1244        let real = |t: u32| word.letter(t).is_none();
1245        match prop {
1246            // (a), (b), (c): a sequence, `strong(seq)` and `weak(seq)`.
1247            Prop::Seq { .. } => Ok(true),
1248            // (d)
1249            Prop::Not(p) => self.nv(*p, &word.flip(), pos, frame, depth, dom, strict),
1250            // (e), (f), (aa)
1251            Prop::And(a, b) => Ok(self.nv(*a, word, pos, frame, depth, dom, strict)?
1252                || self.nv(*b, word, pos, frame, depth, dom, strict)?),
1253            Prop::Or(a, b) => {
1254                if self.ir.nv_rule(id) == Some(NvRule::Implies) {
1255                    // (z): `not p or q` standing for `p implies q`.
1256                    let Prop::Not(p) = self.ir.prop(*a) else {
1257                        unreachable!("an `implies` node's left operand is the negation of `p`")
1258                    };
1259                    return Ok(self.sat(*p, word, pos, frame, depth, dom, strict)?
1260                        && self.nv(*p, word, pos, frame, depth, dom, strict)?
1261                        && self.nv(*b, word, pos, frame, depth, dom, strict)?);
1262                }
1263                Ok(self.nv(*a, word, pos, frame, depth, dom, strict)?
1264                    || self.nv(*b, word, pos, frame, depth, dom, strict)?)
1265            }
1266            // (g): the branch taken, and nothing at all for a missing `else`.
1267            Prop::If { cond, then, els } => {
1268                if self.boolean(*cond, word, pos, frame)? {
1269                    self.nv(*then, word, pos, frame, depth, dom, false)
1270                } else if let Some(e) = els {
1271                    self.nv(*e, word, pos, frame, depth, dom, false)
1272                } else {
1273                    Ok(false)
1274                }
1275            }
1276            // (af): the selected arm, the `default` when none is.
1277            Prop::Case {
1278                selector,
1279                arms,
1280                default,
1281            } => {
1282                for arm in arms {
1283                    if self.arm_selected(*selector, arm, word, pos, frame)? {
1284                        return self.nv(arm.body, word, pos, frame, depth, dom, false);
1285                    }
1286                }
1287                match default {
1288                    Some(d) => self.nv(*d, word, pos, frame, depth, dom, false),
1289                    None => Ok(false),
1290                }
1291            }
1292            // (h), (j), (k): a match of the antecedent with a nonvacuous
1293            // consequent attempt from its end point (the next clock event for the
1294            // nonoverlapped forms).
1295            Prop::Implication {
1296                ante,
1297                cons,
1298                overlapped,
1299            } => {
1300                let on = ante.end_clock;
1301                let dual;
1302                let aw = if self.open_future {
1303                    dual = word.flip();
1304                    &dual
1305                } else {
1306                    word
1307                };
1308                for (end, f) in self.matches(*ante, aw, pos, frame)? {
1309                    if !*overlapped {
1310                        let next = self.after(aw, self.lead_clock(*cons, on), end, 1)?;
1311                        if aw.letter(next) == Some(false) {
1312                            continue;
1313                        }
1314                    }
1315                    if self.nv(*cons, word, end, &f, depth, dom, !*overlapped)? {
1316                        return Ok(true);
1317                    }
1318                }
1319                Ok(!overlapped
1320                    && ante.admits_empty
1321                    && aw.letter(pos) != Some(false)
1322                    && self.nv(*cons, word, pos, frame, depth, dom, false)?)
1323            }
1324            // (l)-(o): the clock event `n` ticks on must exist.
1325            Prop::Nexttime { p, n, .. } => {
1326                let to = self.after(word, dom, pos, *n)?;
1327                Ok(real(to) && self.nv(*p, word, to, frame, depth, dom, false)?)
1328            }
1329            // (p)-(r): a clock event of the range where `p` is nonvacuous, `p`
1330            // not having failed before it.
1331            Prop::Always { p, lo, hi, .. } => {
1332                for t in self.positions(word, dom, pos, *lo, *hi)? {
1333                    if !real(t) {
1334                        break;
1335                    }
1336                    if self.nv(*p, word, t, frame, depth, dom, false)? {
1337                        return Ok(true);
1338                    }
1339                    if !self.sat(*p, word, t, frame, depth, dom, false)? {
1340                        return Ok(false);
1341                    }
1342                }
1343                Ok(false)
1344            }
1345            // (s)-(u): the same with `p` not having held before.
1346            Prop::Eventually { p, lo, hi, .. } => {
1347                for t in self.positions(word, dom, pos, *lo, *hi)? {
1348                    if !real(t) {
1349                        break;
1350                    }
1351                    if self.nv(*p, word, t, frame, depth, dom, false)? {
1352                        return Ok(true);
1353                    }
1354                    if self.sat(*p, word, t, frame, depth, dom, false)? {
1355                        return Ok(false);
1356                    }
1357                }
1358                Ok(false)
1359            }
1360            // (v), (w): a clock event where either operand is nonvacuous, `q` not
1361            // having held and `p` having held at every one before it; (x), (y)
1362            // (`until_with`): only `p` counts.
1363            Prop::Until { p, q, .. } => {
1364                let with = self.ir.nv_rule(id) == Some(NvRule::UntilWith);
1365                let mut t = pos;
1366                while real(t) {
1367                    if self.nv(*p, word, t, frame, depth, dom, false)?
1368                        || (!with && self.nv(*q, word, t, frame, depth, dom, false)?)
1369                    {
1370                        return Ok(true);
1371                    }
1372                    if self.sat(*q, word, t, frame, depth, dom, false)?
1373                        || !self.sat(*p, word, t, frame, depth, dom, false)?
1374                    {
1375                        return Ok(false);
1376                    }
1377                    t = self.align(word, dom, self.after(word, dom, t, 1)?)?;
1378                }
1379                Ok(false)
1380            }
1381            Prop::Clocked { clock, p } => self.nv(*p, word, pos, frame, depth, *clock, strict),
1382            // (ab)-(ae): `p` nonvacuous and the condition true at no time step of
1383            // the attempt (at no clock event, for the `sync_` forms).
1384            Prop::Abort { cond, p, mode } => {
1385                if !self.nv(*p, word, pos, frame, depth, dom, false)? {
1386                    return Ok(false);
1387                }
1388                let sync = mode.is_sync();
1389                let timeline = self.timeline_for(word)?;
1390                for t in pos..word.real {
1391                    let looks = !sync || timeline.as_ref().is_none_or(|tl| tl.is_tick(dom, t));
1392                    if looks && self.boolean(*cond, word, t, frame)? {
1393                        return Ok(false);
1394                    }
1395                    if !sync && timeline.is_none() && self.env.between(*cond, t)? {
1396                        return Ok(false);
1397                    }
1398                }
1399                Ok(true)
1400            }
1401            Prop::Let { writes, body } => {
1402                let mut f = frame.clone();
1403                for (var, leaf) in writes {
1404                    let raw = if word.letter(pos).is_some() {
1405                        0
1406                    } else {
1407                        self.env.value(*leaf, pos, &f)?
1408                    };
1409                    f[var.0 as usize] = Some(self.ir.automaton.local_var(*var).cast(raw));
1410                }
1411                self.nv(*body, word, pos, &f, depth, dom, false)
1412            }
1413            // (i): the instance's body with the actuals substituted.
1414            Prop::Call { decl, args, inits } => {
1415                if depth >= self.recursion_limit(word) {
1416                    return Ok(false);
1417                }
1418                let f = self.call_frame(args, inits, word, pos, frame)?;
1419                let body = self.ir.decls[decl.0 as usize].body;
1420                self.nv(body, word, pos, &f, depth + 1, dom, false)
1421            }
1422        }
1423    }
1424
1425    /// Whether the `case` arm is selected at `pos`: on a `⊤` letter every label
1426    /// holds, on `⊥` none does (the `if (selector === label)` chain of F.3.4.3
1427    /// evaluated at such a letter).
1428    fn arm_selected(
1429        &self,
1430        selector: LeafId,
1431        arm: &CaseArm,
1432        word: &TailWord,
1433        pos: u32,
1434        frame: &Frame,
1435    ) -> Result<bool, TraceError> {
1436        if let Some(top) = word.letter(pos) {
1437            return Ok(top);
1438        }
1439        for &label in &arm.labels {
1440            if self.env.case_match(selector, label, pos, frame)? {
1441                return Ok(true);
1442            }
1443        }
1444        Ok(false)
1445    }
1446
1447    /// The distinct start points `always`/`eventually [lo:hi]` ranges over from
1448    /// `pos`: `pos+lo ..= pos+hi` (to the end for `$`), with every tick past
1449    /// `extent` folded into it.
1450    ///
1451    /// The range counts ticks of `dom` (16.13.2: a bounded `always [1:2]` on a
1452    /// clock is the next one and two ticks of THAT clock).
1453    fn positions(
1454        &self,
1455        word: &TailWord,
1456        dom: ClockId,
1457        pos: u32,
1458        lo: u32,
1459        hi: Option<u32>,
1460    ) -> Result<Vec<u32>, TraceError> {
1461        let ext = word.extent();
1462        let mut out = Vec::new();
1463        let mut k = lo;
1464        loop {
1465            let at = self.after(word, dom, pos, k)?;
1466            if at >= ext {
1467                out.push(ext);
1468                break;
1469            }
1470            out.push(at);
1471            if hi.is_some_and(|h| k >= h) {
1472                break;
1473            }
1474            k += 1;
1475        }
1476        Ok(out)
1477    }
1478
1479    /// How far the verdicts of `id` are SETTLED on `word` (see [`Accel`]'s
1480    /// `Fin`): the window ticks `0..end` have a verdict no continuation of the
1481    /// trace and no choice of word can change, and they are in the accelerator's
1482    /// memo. `None` when the accelerator does not model `id` (several clocks, an
1483    /// unbounded lookahead, a local variable that is read, a word with explicit
1484    /// letters, ...), and the caller then runs the reference code.
1485    ///
1486    /// The memo is filled here, in tick order, with the verdicts the reference
1487    /// evaluation computes (`sat` of `id` at the tick, which is exactly what its
1488    /// loops ask), up to the last tick whose whole lookahead is real samples of
1489    /// `word`; one that is an error stops the filling, and the reference code
1490    /// then meets it where it would have.
1491    fn settled(
1492        &self,
1493        id: PropId,
1494        word: &TailWord,
1495        frame: &Frame,
1496        depth: u32,
1497        dom: ClockId,
1498    ) -> Option<u32> {
1499        let cell = self.accel?;
1500        if !word.mid.is_empty() || self.env.timeline().is_some() {
1501            return None;
1502        }
1503        let (reach, base) = {
1504            let accel = cell.try_borrow().ok()?;
1505            (accel.final_reach(id)?, accel.base())
1506        };
1507        // A tick `t` is settled when `t + reach` is a real tick.
1508        let limit = word.real.saturating_sub(reach);
1509        loop {
1510            let (next, blocked) = cell.try_borrow_mut().ok()?.final_next(id);
1511            let next_rel = u32::try_from(next - base).ok()?;
1512            if blocked || next_rel >= limit {
1513                return Some(next_rel.min(limit));
1514            }
1515            let verdict = self.sat(id, word, next_rel, frame, depth, dom, false);
1516            let mut accel = cell.try_borrow_mut().ok()?;
1517            accel.final_reach(id)?;
1518            accel.final_record(id, next, verdict.ok());
1519        }
1520    }
1521
1522    /// Whether some tick of the range `always`/`eventually [lo:hi]` ranges over
1523    /// from `pos` has the verdict `stop_on` for `p`, found in the order, and with
1524    /// the errors, the reference loop over [`Self::positions`] has: the settled
1525    /// run of the range is read from the memo, and the rest evaluated.
1526    /// `None` declines (see [`Self::settled`]).
1527    #[allow(clippy::too_many_arguments)]
1528    fn scan_range(
1529        &self,
1530        p: PropId,
1531        word: &TailWord,
1532        pos: u32,
1533        lo: u32,
1534        hi: Option<u32>,
1535        frame: &Frame,
1536        depth: u32,
1537        dom: ClockId,
1538        stop_on: bool,
1539    ) -> Option<Result<bool, TraceError>> {
1540        let end = self.settled(p, word, frame, depth, dom)?;
1541        let cell = self.accel?;
1542        let first = pos.saturating_add(lo);
1543        let upper = hi.map_or(end, |h| end.min(pos.saturating_add(h).saturating_add(1)));
1544        let mut k = lo;
1545        if first < upper {
1546            let accel = cell.try_borrow().ok()?;
1547            let base = accel.base();
1548            let hit = accel.final_first(p, stop_on, base + u64::from(first));
1549            if hit.is_some_and(|t| t < base + u64::from(upper)) {
1550                return Some(Ok(true));
1551            }
1552            k = upper - pos;
1553        }
1554        let ext = word.extent();
1555        loop {
1556            if hi.is_some_and(|h| k > h) {
1557                return Some(Ok(false));
1558            }
1559            let at = pos + k;
1560            match self.sat(p, word, at.min(ext), frame, depth, dom, false) {
1561                Err(e) => return Some(Err(e)),
1562                Ok(v) if v == stop_on => return Some(Ok(true)),
1563                Ok(_) => {}
1564            }
1565            if at >= ext || hi.is_some_and(|h| k >= h) {
1566                return Some(Ok(false));
1567            }
1568            k += 1;
1569        }
1570    }
1571
1572    /// The settled run of `p until q` from `pos`: the verdict when a settled tick
1573    /// decides it, else the tick the evaluation resumes at (every tick before
1574    /// it has `q` false and `p` true). `None` declines.
1575    #[allow(clippy::too_many_arguments)]
1576    fn skip_settled_until(
1577        &self,
1578        p: PropId,
1579        q: PropId,
1580        word: &TailWord,
1581        pos: u32,
1582        frame: &Frame,
1583        depth: u32,
1584        dom: ClockId,
1585    ) -> Option<Settled> {
1586        let ep = self.settled(p, word, frame, depth, dom)?;
1587        let eq = self.settled(q, word, frame, depth, dom)?;
1588        let end = ep.min(eq);
1589        if pos >= end {
1590            return None;
1591        }
1592        let accel = self.accel?.try_borrow().ok()?;
1593        let (from, to) = (accel.base() + u64::from(pos), accel.base() + u64::from(end));
1594        let q_true = accel.final_first(q, true, from).filter(|&t| t < to);
1595        let p_false = accel.final_first(p, false, from).filter(|&t| t < to);
1596        Some(match (q_true, p_false) {
1597            // `q` is tried before `p` on a tick.
1598            (Some(a), Some(b)) => Settled::Verdict(a <= b),
1599            (Some(_), None) => Settled::Verdict(true),
1600            (None, Some(_)) => Settled::Verdict(false),
1601            (None, None) => Settled::Resume(end),
1602        })
1603    }
1604
1605    /// `accept_on`/`reject_on`/`sync_*` (16.12.14, F.5.6.1).
1606    #[allow(clippy::too_many_arguments)]
1607    fn abort(
1608        &self,
1609        cond: LeafId,
1610        p: PropId,
1611        mode: AbortMode,
1612        word: &TailWord,
1613        pos: u32,
1614        frame: &Frame,
1615        depth: u32,
1616        dom: ClockId,
1617    ) -> Result<bool, TraceError> {
1618        if mode.accepts() {
1619            self.accept_on(cond, mode.is_sync(), p, false, word, pos, frame, depth, dom)
1620        } else {
1621            // `reject_on(b) P` is `not accept_on(b) not P`.
1622            Ok(!self.accept_on(
1623                cond,
1624                mode.is_sync(),
1625                p,
1626                true,
1627                &word.flip(),
1628                pos,
1629                frame,
1630                depth,
1631                dom,
1632            )?)
1633        }
1634    }
1635
1636    /// `accept_on(cond) p` (`not p` when `negated`) on `word` from `pos`: `p` holds
1637    /// on the whole word and the condition is never true, or for some abort
1638    /// point the word cut just before it and completed with `⊤` satisfies `p`.
1639    /// An abort point is a tick whose sampled condition is true and, unless
1640    /// `sync`, a time step between two ticks where it is.
1641    #[allow(clippy::too_many_arguments)]
1642    fn accept_on(
1643        &self,
1644        cond: LeafId,
1645        sync: bool,
1646        p: PropId,
1647        negated: bool,
1648        word: &TailWord,
1649        pos: u32,
1650        frame: &Frame,
1651        depth: u32,
1652        dom: ClockId,
1653    ) -> Result<bool, TraceError> {
1654        let inner = |w: &TailWord| -> Result<bool, TraceError> {
1655            if negated {
1656                Ok(!self.sat(p, &w.flip(), pos, frame, depth, dom, false)?)
1657            } else {
1658                self.sat(p, w, pos, frame, depth, dom, false)
1659            }
1660        };
1661        let timeline = self.timeline_for(word)?;
1662        let mut aborted = false;
1663        for t in pos..=word.extent() {
1664            // On a multiclock timeline the positions ARE the global steps: an
1665            // asynchronous condition is looked at on every one of them and a
1666            // synchronous one on the ticks of the property's clock. The `between`
1667            // steps do not exist there (every step is a position). When a
1668            // property spans several clocks this uses the clock the abort sits
1669            // on, not each operand's own -- see the module docs' approximation.
1670            let looks = !sync || timeline.as_ref().is_none_or(|tl| tl.is_tick(dom, t));
1671            if looks && self.boolean(cond, word, t, frame)? {
1672                aborted = true;
1673                if inner(&word.cut_then_top(t))? {
1674                    return Ok(true);
1675                }
1676            }
1677            if !sync && timeline.is_none() && t < word.real && self.env.between(cond, t)? {
1678                aborted = true;
1679                if inner(&word.cut_then_top(t + 1))? {
1680                    return Ok(true);
1681                }
1682            }
1683        }
1684        // A condition true on the constant tail is true at `extent` already.
1685        if aborted { Ok(false) } else { inner(word) }
1686    }
1687}
1688
1689/// Whether `p` adds no position of its own: it evaluates its operands at the
1690/// position it is given, so a pending `|=>` synchronization (see
1691/// [`Evaluator::sat`]'s `strict`) passes through it to the operands.
1692fn pass_through(p: &Prop) -> bool {
1693    match p {
1694        Prop::Not(_) | Prop::And(..) | Prop::Or(..) | Prop::Clocked { .. } => true,
1695        Prop::Seq { .. }
1696        | Prop::If { .. }
1697        | Prop::Case { .. }
1698        | Prop::Implication { .. }
1699        | Prop::Nexttime { .. }
1700        | Prop::Always { .. }
1701        | Prop::Eventually { .. }
1702        | Prop::Until { .. }
1703        | Prop::Abort { .. }
1704        | Prop::Let { .. }
1705        | Prop::Call { .. } => false,
1706    }
1707}
1708
1709/// What an automaton entry's sequence is, for the static legality checks of
1710/// 16.12.22.
1711#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1712pub enum SequenceClass {
1713    /// No match at all, empty or not: `(1'b1) intersect (1'b1 ##1 1'b1)`.
1714    Never,
1715    /// Only the empty match: `1'b1[*0]`.
1716    EmptyOnly,
1717    /// At least one nonempty match; `also_empty` says whether the empty match is
1718    /// admitted too (`a[*0:2]`).
1719    NonEmpty { also_empty: bool },
1720}
1721
1722/// Classifies the sequence entered at `seq` (16.12.22, F.5.5: nondegenerate iff
1723/// some nonempty word matches). A nonempty match is a path from the entry to a
1724/// state with a [`Transition::Match`] edge; the Boolean guards are treated as
1725/// satisfiable, which is what "some word" means for a Boolean expression, so
1726/// the result is a pure function of the automaton's graph.
1727///
1728/// `guard_never` names the leaves that can never hold (a constant-false
1729/// Boolean, judged on the leaf's own value -- `!e` for a negated leaf): an edge
1730/// guarded by one is not on any path.
1731pub fn classify_sequence(
1732    au: &Automaton,
1733    seq: SeqRef,
1734    guard_never: &dyn Fn(LeafId) -> bool,
1735) -> SequenceClass {
1736    let mut seen = vec![false; au.states.len()];
1737    let mut stack = vec![seq.entry];
1738    seen[seq.entry.0 as usize] = true;
1739    let mut accepting = false;
1740    while let Some(s) = stack.pop() {
1741        for e in &au.state(s).edges {
1742            if e.guard.is_some_and(guard_never) {
1743                continue;
1744            }
1745            if matches!(e.label, Transition::Match { .. }) {
1746                accepting = true;
1747                continue;
1748            }
1749            if !seen[e.target.0 as usize] {
1750                seen[e.target.0 as usize] = true;
1751                stack.push(e.target);
1752            }
1753        }
1754    }
1755    match (accepting, seq.admits_empty) {
1756        (true, also_empty) => SequenceClass::NonEmpty { also_empty },
1757        (false, true) => SequenceClass::EmptyOnly,
1758        (false, false) => SequenceClass::Never,
1759    }
1760}
1761
1762#[cfg(test)]
1763mod tests {
1764    use super::*;
1765    use crate::sva::ir::LeafId;
1766
1767    /// A property under construction over a hand-built automaton.
1768    struct B {
1769        au: Automaton,
1770        props: Vec<Prop>,
1771        decls: Vec<PropDecl>,
1772    }
1773
1774    impl B {
1775        fn new() -> Self {
1776            B {
1777                au: Automaton::empty(),
1778                props: Vec::new(),
1779                decls: Vec::new(),
1780            }
1781        }
1782
1783        fn prop(&mut self, p: Prop) -> PropId {
1784            self.props.push(p);
1785            PropId(self.props.len() as u32 - 1)
1786        }
1787
1788        /// The sequence `l0 ##d1 l1 ##d2 l2 ...` of Boolean leaves.
1789        fn seq(&mut self, leaves: &[u32], delays: &[u32]) -> SeqRef {
1790            let ranges: Vec<(u32, Option<u32>)> = delays.iter().map(|&d| (d, Some(d))).collect();
1791            self.seq_r(leaves, &ranges)
1792        }
1793
1794        /// The sequence `l0 ##[m1:n1] l1 ...` (`None` is `$`).
1795        fn seq_r(&mut self, leaves: &[u32], delays: &[(u32, Option<u32>)]) -> SeqRef {
1796            assert_eq!(leaves.len(), delays.len() + 1);
1797            let entry = self.au.new_state();
1798            let mut at = entry;
1799            for (i, l) in leaves.iter().enumerate() {
1800                let next = self.au.new_state();
1801                self.au
1802                    .add_edge(at, Some(LeafId(*l)), Transition::Epsilon, next);
1803                at = next;
1804                if let Some(&(min, max)) = delays.get(i) {
1805                    let next = self.au.new_state();
1806                    self.au
1807                        .add_edge(at, None, Transition::Delay { min, max }, next);
1808                    at = next;
1809                }
1810            }
1811            self.au.add_edge(
1812                at,
1813                None,
1814                Transition::Match {
1815                    actions: Vec::new(),
1816                },
1817                at,
1818            );
1819            SeqRef::single(entry, false)
1820        }
1821
1822        fn leaf(&mut self, l: u32) -> SeqRef {
1823            self.seq(&[l], &[])
1824        }
1825
1826        /// The Boolean leaf `l` as a property (a one-letter sequence).
1827        fn b(&mut self, l: u32, strength: Option<Strength>) -> PropId {
1828            let seq = self.leaf(l);
1829            self.prop(Prop::Seq { seq, strength })
1830        }
1831
1832        fn not(&mut self, p: PropId) -> PropId {
1833            self.prop(Prop::Not(p))
1834        }
1835
1836        fn finish(self, root: PropId) -> PropertyIr {
1837            PropertyIr {
1838                automaton: self.au,
1839                props: self.props,
1840                decls: self.decls,
1841                root,
1842                disable: None,
1843                nv_rules: Vec::new(),
1844            }
1845        }
1846    }
1847
1848    /// Leaves as per-leaf boolean rows.
1849    struct Rows(Vec<Vec<bool>>);
1850
1851    impl PropEnv for Rows {
1852        fn value(&self, leaf: LeafId, tick: u32, _: &[Option<i64>]) -> Result<i64, TraceError> {
1853            Ok(i64::from(self.0[leaf.0 as usize][tick as usize]))
1854        }
1855        fn case_match(
1856            &self,
1857            selector: LeafId,
1858            label: LeafId,
1859            tick: u32,
1860            f: &[Option<i64>],
1861        ) -> Result<bool, TraceError> {
1862            Ok(self.value(selector, tick, f)? == self.value(label, tick, f)?)
1863        }
1864    }
1865
1866    fn rows(r: &[&str]) -> Rows {
1867        Rows(
1868            r.iter()
1869                .map(|s| s.chars().map(|c| c == '1').collect())
1870                .collect(),
1871        )
1872    }
1873
1874    fn verdict(ir: &PropertyIr, env: &Rows, len: u32, start: u32, strong: bool) -> Verdict {
1875        Evaluator::new(ir, env, strong).verdict(len, start).unwrap()
1876    }
1877
1878    /// 16.12.2: `p3` is `b ##1 c`; `a1: assert property (a |-> p3)` reads `p3` as
1879    /// weak, `c1: cover property (a #-# p3)` as strong.
1880    #[test]
1881    fn lrm_sequence_property_is_weak_in_assert_and_strong_in_cover() {
1882        // leaves: 0 = a, 1 = b, 2 = c
1883        let mut b = B::new();
1884        let a = b.leaf(0);
1885        let p3 = b.seq(&[1, 2], &[1]);
1886        let p3p = b.prop(Prop::Seq {
1887            seq: p3,
1888            strength: None,
1889        });
1890        let a1 = b.prop(Prop::Implication {
1891            ante: a,
1892            cons: p3p,
1893            overlapped: true,
1894        });
1895        let ir = b.finish(a1);
1896        // a false: true either way. b true then the trace ends: weak holds,
1897        // strong does not (assert only needs the weak reading).
1898        let end_after_b = rows(&["1", "1", "0"]);
1899        assert_eq!(
1900            verdict(&ir, &end_after_b, 1, 0, false),
1901            Verdict::Holds,
1902            "weak: `b` holds and there is no next tick"
1903        );
1904        // b true and c true at the next tick: holds strongly.
1905        let full = rows(&["11", "10", "01"]);
1906        assert_eq!(verdict(&ir, &full, 2, 0, false), Verdict::HoldsStrongly);
1907        // b true but c false at the next tick: fails.
1908        let bad = rows(&["11", "10", "00"]);
1909        assert_eq!(verdict(&ir, &bad, 2, 0, false), Verdict::Fails);
1910        // a false at the start: the implication is vacuously true.
1911        let vac = rows(&["0", "0", "0"]);
1912        assert_eq!(verdict(&ir, &vac, 1, 0, false), Verdict::HoldsStrongly);
1913        // b false at the start: fails regardless of the trace length.
1914        let nob = rows(&["1", "0", "1"]);
1915        assert_eq!(verdict(&ir, &nob, 1, 0, false), Verdict::Fails);
1916    }
1917
1918    /// Every assignment of `leaves` boolean rows of `len` ticks.
1919    fn all_traces(leaves: usize, len: usize) -> Vec<Rows> {
1920        let bits = leaves * len;
1921        (0..1u32 << bits)
1922            .map(|m| {
1923                Rows(
1924                    (0..leaves)
1925                        .map(|l| (0..len).map(|t| m >> (l * len + t) & 1 == 1).collect())
1926                        .collect(),
1927                )
1928            })
1929            .collect()
1930    }
1931
1932    /// The verdict of `root` over every trace of `leaves` signals of 0..=max_len ticks
1933    /// from every start tick, as a flat list.
1934    fn verdicts(ir: &PropertyIr, leaves: usize, max_len: usize, strong: bool) -> Vec<Verdict> {
1935        let mut out = Vec::new();
1936        for len in 1..=max_len {
1937            for env in all_traces(leaves, len) {
1938                for start in 0..len as u32 {
1939                    out.push(verdict(ir, &env, len as u32, start, strong));
1940                }
1941            }
1942        }
1943        out
1944    }
1945
1946    /// 16.12.3: `not` switches strength. For `a ##[1:$] b` the weak sequence holds on
1947    /// the dual word (an unbounded delay is never refuted by a finite prefix),
1948    /// so `not (a ##[1:$] b)` fails on a trace that ends right after `a` -- the
1949    /// pitfall -- while `not strong(a ##[1:$] b)` only stays pending.
1950    #[test]
1951    fn lrm_not_of_a_weak_sequence_is_the_pitfall_and_strong_is_the_fix() {
1952        let mut b = B::new();
1953        let r = b.seq_r(&[0, 1], &[(1, None)]);
1954        let weak = b.prop(Prop::Seq {
1955            seq: r,
1956            strength: None,
1957        });
1958        let a1 = b.not(weak);
1959        let strong = b.prop(Prop::Seq {
1960            seq: r,
1961            strength: Some(Strength::Strong),
1962        });
1963        let a2 = b.not(strong);
1964        let mut ir1 = b.finish(a1);
1965        // a holds at the last tick of the trace, `b` never does.
1966        let env = rows(&["1", "0"]);
1967        assert_eq!(verdict(&ir1, &env, 1, 0, false), Verdict::Fails);
1968        ir1.root = a2;
1969        assert_eq!(verdict(&ir1, &env, 1, 0, false), Verdict::Holds);
1970        // With `b` seen after `a`, the strong sequence matched: `not` fails both ways.
1971        let env = rows(&["10", "01"]);
1972        assert_eq!(verdict(&ir1, &env, 2, 0, false), Verdict::Fails);
1973    }
1974
1975    /// Table 40-3 (`aundf`): the weak `a ##[1:$] b` never fails on a finite prefix
1976    /// and does not hold STRONGLY until it matched, so its attempt is pending
1977    /// (not passed) while `b` has not been seen, and passes once it has.
1978    #[test]
1979    fn weak_unbounded_sequence_is_pending_until_it_matches() {
1980        let mut b = B::new();
1981        let r = b.seq_r(&[0, 1], &[(1, None)]);
1982        let weak = b.prop(Prop::Seq {
1983            seq: r,
1984            strength: None,
1985        });
1986        let ir = b.finish(weak);
1987        // Leaves 0 = a, 1 = b, one string per leaf: `a` at tick 0, `b` never.
1988        for (a, bb) in [("1", "0"), ("10", "00"), ("100", "000")] {
1989            let env = rows(&[a, bb]);
1990            assert_eq!(verdict(&ir, &env, a.len() as u32, 0, false), Verdict::Holds);
1991        }
1992        // `b` two ticks after `a`.
1993        let env = rows(&["100", "001"]);
1994        assert_eq!(verdict(&ir, &env, 3, 0, false), Verdict::HoldsStrongly);
1995    }
1996
1997    /// Strength is the only thing `not` changes: `strong(R)` implies `weak(R)` on
1998    /// every trace, and `weak` fails only where `strong` does.
1999    #[test]
2000    fn strong_sequence_implies_weak_sequence() {
2001        let mut b = B::new();
2002        let r = b.seq_r(&[0, 1], &[(1, Some(2))]);
2003        let s = b.prop(Prop::Seq {
2004            seq: r,
2005            strength: Some(Strength::Strong),
2006        });
2007        let w = b.prop(Prop::Seq {
2008            seq: r,
2009            strength: Some(Strength::Weak),
2010        });
2011        let mut ir = b.finish(s);
2012        let strong = verdicts(&ir, 2, 4, false);
2013        ir.root = w;
2014        let weak = verdicts(&ir, 2, 4, false);
2015        assert_eq!(strong.len(), weak.len());
2016        for (s, w) in strong.iter().zip(&weak) {
2017            // strong: Fails or HoldsStrongly... a strong sequence is `Holds`
2018            // (pending) on a trace that cut it short, weak is too.
2019            assert!(
2020                *s != Verdict::HoldsStrongly || *w == Verdict::HoldsStrongly,
2021                "strong match on a trace the weak reading does not hold strongly"
2022            );
2023            assert!(*s == Verdict::Fails || *w != Verdict::Fails);
2024        }
2025    }
2026
2027    /// 16.12.22 nondegeneracy, the LRM's three examples as automata.
2028    #[test]
2029    fn classify_sequence_lrm_examples() {
2030        let never = |_: LeafId| false;
2031        // (1'b1) intersect (1'b1 ##1 1'b1): no word matches -- built as an
2032        // entry whose only edges lead nowhere near a match.
2033        let mut au = Automaton::empty();
2034        let e = au.new_state();
2035        let dead = au.new_state();
2036        au.add_edge(e, Some(LeafId(0)), Transition::Epsilon, dead);
2037        let r = SeqRef::single(e, false);
2038        assert_eq!(classify_sequence(&au, r, &never), SequenceClass::Never);
2039        // 1'b1[*0]: only the empty match.
2040        let r = SeqRef::single(e, true);
2041        assert_eq!(classify_sequence(&au, r, &never), SequenceClass::EmptyOnly);
2042        // a[*0:2]: a nonempty match and the empty one.
2043        let mut b = B::new();
2044        let mut seq = b.leaf(0);
2045        seq.admits_empty = true;
2046        assert_eq!(
2047            classify_sequence(&b.au, seq, &never),
2048            SequenceClass::NonEmpty { also_empty: true }
2049        );
2050        // A match behind a leaf that can never hold is no match.
2051        let mut seq = b.leaf(1);
2052        assert_eq!(
2053            classify_sequence(&b.au, seq, &|l| l == LeafId(1)),
2054            SequenceClass::Never
2055        );
2056        seq.admits_empty = false;
2057        assert_eq!(
2058            classify_sequence(&b.au, seq, &never),
2059            SequenceClass::NonEmpty { also_empty: false }
2060        );
2061    }
2062
2063    /// `s_nexttime p` is `not nexttime not p` (F.3.4.3), for every trace, start and
2064    /// offset; `nexttime[0]` is the alignment operator (16.12.10).
2065    #[test]
2066    fn nexttime_duality_and_alignment() {
2067        for n in 0..=2u32 {
2068            for strength in [Some(Strength::Weak), Some(Strength::Strong)] {
2069                let mut b = B::new();
2070                let p = b.b(0, strength);
2071                let snt = b.prop(Prop::Nexttime { p, n, strong: true });
2072                let np = b.not(p);
2073                let nn = b.prop(Prop::Nexttime {
2074                    p: np,
2075                    n,
2076                    strong: false,
2077                });
2078                let dual = b.not(nn);
2079                let mut ir = b.finish(snt);
2080                let a = verdicts(&ir, 1, 4, false);
2081                ir.root = dual;
2082                assert_eq!(a, verdicts(&ir, 1, 4, false), "n = {n}");
2083            }
2084        }
2085        // nexttime p needs the next tick: on a single-tick trace the weak
2086        // reading is satisfied, the strong one is only pending.
2087        let mut b = B::new();
2088        let p = b.b(0, Some(Strength::Strong));
2089        let nt = b.prop(Prop::Nexttime {
2090            p,
2091            n: 1,
2092            strong: false,
2093        });
2094        let ir = b.finish(nt);
2095        assert_eq!(verdict(&ir, &rows(&["0"]), 1, 0, false), Verdict::Holds);
2096        assert_eq!(
2097            verdict(&ir, &rows(&["01"]), 2, 0, false),
2098            Verdict::HoldsStrongly
2099        );
2100        assert_eq!(verdict(&ir, &rows(&["10"]), 2, 0, false), Verdict::Fails);
2101    }
2102
2103    /// `s_eventually p` is `not always not p` and `always p` is `p until 0`.
2104    #[test]
2105    fn always_eventually_until_dualities() {
2106        let mut b = B::new();
2107        let p = b.b(0, Some(Strength::Weak));
2108        let seventually = b.prop(Prop::Eventually {
2109            p,
2110            lo: 0,
2111            hi: None,
2112            strong: true,
2113        });
2114        let np = b.not(p);
2115        let al = b.prop(Prop::Always {
2116            p: np,
2117            lo: 0,
2118            hi: None,
2119            strong: false,
2120        });
2121        let dual = b.not(al);
2122        let always = b.prop(Prop::Always {
2123            p,
2124            lo: 0,
2125            hi: None,
2126            strong: false,
2127        });
2128        let zero = b.b(1, Some(Strength::Weak));
2129        let until = b.prop(Prop::Until {
2130            p,
2131            q: zero,
2132            strong: false,
2133        });
2134        let mut ir = b.finish(seventually);
2135        let a = verdicts(&ir, 2, 3, false);
2136        ir.root = dual;
2137        assert_eq!(a, verdicts(&ir, 2, 3, false));
2138        // `until` with q the constant-0 leaf 1 only when leaf 1 is all-zero; compare
2139        // `always p` with `p until q` on the traces where q never holds.
2140        ir.root = always;
2141        let al = verdicts_with(&ir, 3);
2142        ir.root = until;
2143        let un = verdicts_with(&ir, 3);
2144        assert_eq!(al, un);
2145    }
2146
2147    /// Verdicts over every `p` trace with `q` constant 0.
2148    fn verdicts_with(ir: &PropertyIr, max_len: usize) -> Vec<Verdict> {
2149        let mut out = Vec::new();
2150        for len in 1..=max_len {
2151            for env in all_traces(1, len) {
2152                let env = Rows(vec![env.0[0].clone(), vec![false; len]]);
2153                for start in 0..len as u32 {
2154                    out.push(verdict(ir, &env, len as u32, start, false));
2155                }
2156            }
2157        }
2158        out
2159    }
2160
2161    /// 16.12.12: the four `until` forms on the LRM's `a`/`b` traces.
2162    #[test]
2163    fn until_family_truth_table() {
2164        // p until q: q at some tick j with p at all earlier ticks, or p always.
2165        let mut b = B::new();
2166        let p = b.b(0, Some(Strength::Weak));
2167        let q = b.b(1, Some(Strength::Weak));
2168        let u = b.prop(Prop::Until {
2169            p,
2170            q,
2171            strong: false,
2172        });
2173        let su = b.prop(Prop::Until { p, q, strong: true });
2174        let mut ir = b.finish(u);
2175        // a,a,a... never b: weak until holds (pending), strong until does not.
2176        let env = rows(&["111", "000"]);
2177        assert_eq!(verdict(&ir, &env, 3, 0, false), Verdict::Holds);
2178        ir.root = su;
2179        assert_eq!(verdict(&ir, &env, 3, 0, false), Verdict::Holds);
2180        // p fails before q: fails both.
2181        let env = rows(&["101", "001"]);
2182        assert_eq!(verdict(&ir, &env, 3, 0, false), Verdict::Fails);
2183        // q arrives while p still holds: holds strongly, even the strong form.
2184        let env = rows(&["110", "001"]);
2185        assert_eq!(verdict(&ir, &env, 3, 0, false), Verdict::HoldsStrongly);
2186        // q at the same tick p drops: `until` does not need p at the q tick.
2187        let env = rows(&["100", "010"]);
2188        assert_eq!(verdict(&ir, &env, 3, 0, false), Verdict::HoldsStrongly);
2189    }
2190
2191    /// 16.12.14: the abort cut. `accept_on(a) p` succeeds if `a` becomes true
2192    /// during the evaluation even when `p` would fail afterwards; and an abort
2193    /// that is not reached leaves `p`'s verdict.
2194    #[test]
2195    fn accept_on_cuts_the_word_at_the_abort() {
2196        // p = strong(x ##1 y), abort cond = leaf 2.
2197        let mut b = B::new();
2198        let r = b.seq(&[0, 1], &[1]);
2199        let p = b.prop(Prop::Seq {
2200            seq: r,
2201            strength: Some(Strength::Strong),
2202        });
2203        let acc = b.prop(Prop::Abort {
2204            cond: LeafId(2),
2205            p,
2206            mode: AbortMode::SyncAccept,
2207        });
2208        let ir = b.finish(acc);
2209        // x then y: holds strongly regardless.
2210        assert_eq!(
2211            verdict(&ir, &rows(&["10", "01", "00"]), 2, 0, false),
2212            Verdict::HoldsStrongly
2213        );
2214        // the sequence fails at tick 1 (y low) but the abort fires at tick 1: the
2215        // word cut before tick 1 and completed by T lets `y` be true, so true.
2216        let env = rows(&["10", "00", "01"]);
2217        assert_eq!(verdict(&ir, &env, 2, 0, false), Verdict::HoldsStrongly);
2218        // the abort fires only after the sequence failed: cut at tick 2 still sees the
2219        // failure at tick 1.
2220        let env = rows(&["100", "000", "001"]);
2221        assert_eq!(verdict(&ir, &env, 3, 0, false), Verdict::Fails);
2222        // sync_reject_on is `not sync_accept_on not`: on the same trace it is false when
2223        // the abort fires before the property is decided, true if it never fires.
2224        let rej = {
2225            let mut b = B::new();
2226            let r = b.seq(&[0, 1], &[1]);
2227            let p = b.prop(Prop::Seq {
2228                seq: r,
2229                strength: Some(Strength::Strong),
2230            });
2231            let rej = b.prop(Prop::Abort {
2232                cond: LeafId(2),
2233                p,
2234                mode: AbortMode::SyncReject,
2235            });
2236            b.finish(rej)
2237        };
2238        let env = rows(&["10", "01", "01"]);
2239        assert_eq!(verdict(&rej, &env, 2, 0, false), Verdict::Fails);
2240        let env = rows(&["10", "01", "00"]);
2241        assert_eq!(verdict(&rej, &env, 2, 0, false), Verdict::HoldsStrongly);
2242    }
2243
2244    /// 16.12.14: an abort and the end of the evaluation at the same tick: the abort
2245    /// wins; the outermost of two nested aborts wins a tie.
2246    #[test]
2247    fn abort_wins_a_tie_and_the_outermost_nested_abort_wins() {
2248        // p1 = strong(x), decided at tick 0. a (leaf 1) fires at tick 0 too.
2249        let mut b = B::new();
2250        let x = b.b(0, Some(Strength::Strong));
2251        let acc = b.prop(Prop::Abort {
2252            cond: LeafId(1),
2253            p: x,
2254            mode: AbortMode::SyncAccept,
2255        });
2256        let rej = b.prop(Prop::Abort {
2257            cond: LeafId(1),
2258            p: x,
2259            mode: AbortMode::SyncReject,
2260        });
2261        // x is false but the abort fires at tick 0 along with it: accept wins (true),
2262        // reject wins (false).
2263        let env = rows(&["0", "1"]);
2264        let mut ir = b.finish(acc);
2265        assert_eq!(verdict(&ir, &env, 1, 0, false), Verdict::HoldsStrongly);
2266        ir.root = rej;
2267        assert_eq!(verdict(&ir, &env, 1, 0, false), Verdict::Fails);
2268        // x true at tick 0 with the abort too: reject_on still fails (the abort takes
2269        // precedence), accept_on holds.
2270        let env = rows(&["1", "1"]);
2271        assert_eq!(verdict(&ir, &env, 1, 0, false), Verdict::Fails);
2272        // nested accept_on(a) reject_on(b) p1: a and b at the same tick -> succeeds;
2273        // b before a -> fails.
2274        let mut b = B::new();
2275        // p1 = strong(x ##5 x) is still being evaluated through all three ticks.
2276        let r = b.seq(&[0, 0], &[5]);
2277        let p1 = b.prop(Prop::Seq {
2278            seq: r,
2279            strength: Some(Strength::Strong),
2280        });
2281        let inner = b.prop(Prop::Abort {
2282            cond: LeafId(2),
2283            p: p1,
2284            mode: AbortMode::SyncReject,
2285        });
2286        let outer = b.prop(Prop::Abort {
2287            cond: LeafId(1),
2288            p: inner,
2289            mode: AbortMode::SyncAccept,
2290        });
2291        let ir = b.finish(outer);
2292        // leaves: 0 = x (always true), 1 = a, 2 = b
2293        let same = rows(&["111", "001", "001"]);
2294        assert_eq!(verdict(&ir, &same, 3, 0, false), Verdict::HoldsStrongly);
2295        let b_first = rows(&["111", "001", "010"]);
2296        assert_eq!(verdict(&ir, &b_first, 3, 0, false), Verdict::Fails);
2297        // a before b: the outer abort already accepted.
2298        let a_first = rows(&["111", "010", "001"]);
2299        assert_eq!(verdict(&ir, &a_first, 3, 0, false), Verdict::HoldsStrongly);
2300    }
2301
2302    /// 16.12.14: `async` aborts also see conditions between ticks; the sync forms do
2303    /// not. The three formulations of the LRM example agree on tick-aligned traces,
2304    /// and only the async form reacts to a pulse between ticks.
2305    #[test]
2306    fn async_abort_sees_between_ticks_and_sync_does_not() {
2307        struct Between(Rows, Vec<u32>);
2308        impl PropEnv for Between {
2309            fn value(&self, l: LeafId, t: u32, f: &[Option<i64>]) -> Result<i64, TraceError> {
2310                self.0.value(l, t, f)
2311            }
2312            fn case_match(
2313                &self,
2314                s: LeafId,
2315                l: LeafId,
2316                t: u32,
2317                f: &[Option<i64>],
2318            ) -> Result<bool, TraceError> {
2319                self.0.case_match(s, l, t, f)
2320            }
2321            fn between(&self, leaf: LeafId, after: u32) -> Result<bool, TraceError> {
2322                Ok(leaf == LeafId(1) && self.1.contains(&after))
2323            }
2324        }
2325        let build = |mode| {
2326            let mut b = B::new();
2327            let r = b.seq(&[0, 2], &[1]);
2328            let p = b.prop(Prop::Seq {
2329                seq: r,
2330                strength: Some(Strength::Strong),
2331            });
2332            let r = b.prop(Prop::Abort {
2333                cond: LeafId(1),
2334                p,
2335                mode,
2336            });
2337            b.finish(r)
2338        };
2339        // x ##1 y with y never true; the abort pulses between tick 0 and tick 1 only.
2340        let fails = Between(rows(&["10", "00", "00"]), vec![0]);
2341        let ev =
2342            |ir: &PropertyIr, env: &Between| Evaluator::new(ir, env, false).verdict(2, 0).unwrap();
2343        // accept_on takes the pulse (the cut before tick 1 leaves `y` to the T tail);
2344        // sync_accept_on never samples it and `y` fails.
2345        assert_eq!(
2346            ev(&build(AbortMode::Accept), &fails),
2347            Verdict::HoldsStrongly
2348        );
2349        assert_eq!(ev(&build(AbortMode::SyncAccept), &fails), Verdict::Fails);
2350        // x ##1 y holds; the same pulse rejects it asynchronously, not synchronously.
2351        let holds = Between(rows(&["10", "00", "01"]), vec![0]);
2352        assert_eq!(ev(&build(AbortMode::Reject), &holds), Verdict::Fails);
2353        assert_eq!(
2354            ev(&build(AbortMode::SyncReject), &holds),
2355            Verdict::HoldsStrongly
2356        );
2357    }
2358
2359    /// `if`/`case` on the two-valued letters: `⊤` takes the first arm, `⊥` the default.
2360    #[test]
2361    fn if_and_case_follow_the_condition() {
2362        let mut b = B::new();
2363        let t = b.b(1, Some(Strength::Strong));
2364        let e = b.b(2, Some(Strength::Strong));
2365        let i = b.prop(Prop::If {
2366            cond: LeafId(0),
2367            then: t,
2368            els: Some(e),
2369        });
2370        let ir = b.finish(i);
2371        assert_eq!(
2372            verdict(&ir, &rows(&["1", "1", "0"]), 1, 0, false),
2373            Verdict::HoldsStrongly
2374        );
2375        assert_eq!(
2376            verdict(&ir, &rows(&["0", "1", "0"]), 1, 0, false),
2377            Verdict::Fails
2378        );
2379        // case (l0) l1: p1; l2: p2; default: p3
2380        let mut b = B::new();
2381        let p1 = b.b(3, Some(Strength::Strong));
2382        let p2 = b.b(4, Some(Strength::Strong));
2383        let p3 = b.b(5, Some(Strength::Strong));
2384        let c = b.prop(Prop::Case {
2385            selector: LeafId(0),
2386            arms: vec![
2387                CaseArm {
2388                    labels: vec![LeafId(1)],
2389                    body: p1,
2390                },
2391                CaseArm {
2392                    labels: vec![LeafId(2)],
2393                    body: p2,
2394                },
2395            ],
2396            default: Some(p3),
2397        });
2398        let ir = b.finish(c);
2399        // selector equals label 2 only: p2.
2400        let env = rows(&["1", "0", "1", "0", "1", "0"]);
2401        assert_eq!(verdict(&ir, &env, 1, 0, false), Verdict::HoldsStrongly);
2402        // equals none: default p3 (selector 1, labels 0 and 0).
2403        let env = rows(&["1", "0", "0", "1", "1", "0"]);
2404        assert_eq!(verdict(&ir, &env, 1, 0, false), Verdict::Fails);
2405    }
2406
2407    // ---- multiclock (16.13; sva T28) -------------------------------------------
2408
2409    /// Leaf rows over the global steps of a trace, plus the tick lists of its
2410    /// clocks: `ClockId(c)` ticks at the steps where `ticks[c]` has a `1`.
2411    struct Multi {
2412        rows: Rows,
2413        timeline: Timeline,
2414    }
2415
2416    impl PropEnv for Multi {
2417        fn value(&self, leaf: LeafId, tick: u32, f: &[Option<i64>]) -> Result<i64, TraceError> {
2418            self.rows.value(leaf, tick, f)
2419        }
2420        fn case_match(
2421            &self,
2422            selector: LeafId,
2423            label: LeafId,
2424            tick: u32,
2425            f: &[Option<i64>],
2426        ) -> Result<bool, TraceError> {
2427            self.rows.case_match(selector, label, tick, f)
2428        }
2429        fn timeline(&self) -> Option<&Timeline> {
2430            Some(&self.timeline)
2431        }
2432    }
2433
2434    fn multi(leaves: &[&str], clocks: &[&str]) -> Multi {
2435        let bits: Vec<Vec<bool>> = clocks
2436            .iter()
2437            .map(|s| s.chars().map(|c| c == '1').collect())
2438            .collect();
2439        let refs: Vec<&[bool]> = bits.iter().map(Vec::as_slice).collect();
2440        Multi {
2441            rows: rows(leaves),
2442            timeline: Timeline::from_rows(&refs),
2443        }
2444    }
2445
2446    fn multi_verdict(ir: &PropertyIr, env: &Multi) -> Verdict {
2447        Evaluator::new(ir, env, false)
2448            .verdict(env.timeline.real(), 0)
2449            .unwrap()
2450    }
2451
2452    impl B {
2453        /// The one-letter sequence `leaf` on `clock`.
2454        fn leaf_on(&mut self, l: u32, clock: u32) -> SeqRef {
2455            let mut s = self.leaf(l);
2456            s.clock = ClockId(clock);
2457            s.end_clock = ClockId(clock);
2458            s
2459        }
2460
2461        /// `l0 ##d @(clock to) l1`: the cross-clock synchronisation of 16.13.1.
2462        fn seq_sync(&mut self, l0: u32, l1: u32, to: u32, strict: bool) -> SeqRef {
2463            let entry = self.au.new_state();
2464            let s1 = self.au.new_state();
2465            let s2 = self.au.new_state();
2466            let s3 = self.au.new_state();
2467            self.au
2468                .add_edge(entry, Some(LeafId(l0)), Transition::Epsilon, s1);
2469            self.au.add_edge(
2470                s1,
2471                None,
2472                Transition::ClockSync {
2473                    to: ClockId(to),
2474                    strict,
2475                },
2476                s2,
2477            );
2478            self.au
2479                .add_edge(s2, Some(LeafId(l1)), Transition::Epsilon, s3);
2480            self.au.add_edge(
2481                s3,
2482                None,
2483                Transition::Match {
2484                    actions: Vec::new(),
2485                },
2486                s3,
2487            );
2488            SeqRef {
2489                entry,
2490                admits_empty: false,
2491                clock: ClockId(0),
2492                end_clock: ClockId(to),
2493            }
2494        }
2495    }
2496
2497    /// 16.13.2: `@(clk) a |-> @(clk1) b` (clk = ClockId 0, clk1 = ClockId 1). The
2498    /// consequent starts at the nearest tick of clk1 at or after the end of the
2499    /// antecedent.
2500    #[test]
2501    fn overlapped_implication_awaits_the_nearest_tick_of_the_consequent_clock() {
2502        let mut b = B::new();
2503        let a = b.leaf_on(0, 0);
2504        let cons = b.leaf_on(1, 1);
2505        let c = b.prop(Prop::Seq {
2506            seq: cons,
2507            strength: Some(Strength::Strong),
2508        });
2509        let c = b.prop(Prop::Clocked {
2510            clock: ClockId(1),
2511            p: c,
2512        });
2513        let root = b.prop(Prop::Implication {
2514            ante: a,
2515            cons: c,
2516            overlapped: true,
2517        });
2518        let ir = b.finish(root);
2519        // clk ticks at 0 and 2, clk1 at 1 and 3; `a` holds at 0.
2520        let clocks = ["1010", "0101"];
2521        // b at clk1's first tick (step 1): holds.
2522        let env = multi(&["1000", "0100"], &clocks);
2523        assert_eq!(multi_verdict(&ir, &env), Verdict::HoldsStrongly);
2524        // b only at clk1's second tick (step 3): the consequent is checked at
2525        // step 1 and fails.
2526        let env = multi(&["1000", "0001"], &clocks);
2527        assert_eq!(multi_verdict(&ir, &env), Verdict::Fails);
2528        // b true at a step that is no tick of clk1 does not count.
2529        let env = multi(&["1000", "1010"], &clocks);
2530        assert_eq!(multi_verdict(&ir, &env), Verdict::Fails);
2531    }
2532
2533    /// 16.13.2: `|=>` moves to the nearest STRICTLY LATER tick of the consequent's
2534    /// clock. Annex F's rewrite (`ante ##1 1 |-> cons`) advances one tick of the
2535    /// ANTECEDENT's clock first; the two differ when a clk1 tick falls between the
2536    /// end of the antecedent and the next clk tick, and the prose of 16.13.2 is the
2537    /// reading pinned here: `b` false at step 1 (the strictly later clk1 tick) fails
2538    /// even though it holds at step 2, which the rewrite would have checked.
2539    #[test]
2540    fn non_overlapped_implication_moves_to_the_strictly_later_consequent_tick() {
2541        let mut b = B::new();
2542        let a = b.leaf_on(0, 0);
2543        let cons = b.leaf_on(1, 1);
2544        let c = b.prop(Prop::Seq {
2545            seq: cons,
2546            strength: Some(Strength::Strong),
2547        });
2548        let c = b.prop(Prop::Clocked {
2549            clock: ClockId(1),
2550            p: c,
2551        });
2552        let root = b.prop(Prop::Implication {
2553            ante: a,
2554            cons: c,
2555            overlapped: false,
2556        });
2557        let ir = b.finish(root);
2558        // clk ticks at 0 and 2, clk1 at every step.
2559        let clocks = ["1010", "1111"];
2560        let env = multi(&["1000", "0100"], &clocks);
2561        assert_eq!(multi_verdict(&ir, &env), Verdict::HoldsStrongly);
2562        let env = multi(&["1000", "0010"], &clocks);
2563        assert_eq!(
2564            multi_verdict(&ir, &env),
2565            Verdict::Fails,
2566            "the consequent is checked at step 1, the next clk1 tick after the antecedent"
2567        );
2568        // clk1's tick at the end point itself (step 0) is not "strictly later":
2569        let env = multi(&["1000", "1000"], &clocks);
2570        assert_eq!(multi_verdict(&ir, &env), Verdict::Fails);
2571    }
2572
2573    /// 16.13.1: `a ##1 @(clk1) b` waits for the first tick of clk1 strictly after
2574    /// the tick `a` matched at; `##0` allows the same step.
2575    #[test]
2576    fn cross_clock_delay_synchronises_to_the_other_clock() {
2577        let clocks = ["10100", "01011"];
2578        let mut b = B::new();
2579        let s = b.seq_sync(0, 1, 1, true);
2580        let root = b.prop(Prop::Seq {
2581            seq: s,
2582            strength: Some(Strength::Strong),
2583        });
2584        let ir = b.finish(root);
2585        // `a` at step 0; clk1's first tick strictly after is step 1.
2586        let env = multi(&["10000", "01000"], &clocks);
2587        assert_eq!(multi_verdict(&ir, &env), Verdict::HoldsStrongly);
2588        // `b` at step 3 (a later clk1 tick) does not match: the sync is to the
2589        // NEAREST tick.
2590        let env = multi(&["10000", "00010"], &clocks);
2591        assert_eq!(multi_verdict(&ir, &env), Verdict::Fails);
2592        // A non-strict (`##0`) sync with both clocks ticking at step 0 stays there.
2593        let clocks = ["1010", "1101"];
2594        let mut b = B::new();
2595        let s = b.seq_sync(0, 1, 1, false);
2596        let root = b.prop(Prop::Seq {
2597            seq: s,
2598            strength: Some(Strength::Strong),
2599        });
2600        let ir = b.finish(root);
2601        let env = multi(&["1000", "1000"], &clocks);
2602        assert_eq!(multi_verdict(&ir, &env), Verdict::HoldsStrongly);
2603    }
2604
2605    mod calls;
2606    mod incremental;
2607    mod nonvacuity;
2608}