Skip to main content

sv_sim_sim/sva/
incremental.rs

1//! Incremental sequence evaluation for the live engine (`sva-perf`).
2//!
3//! The reference evaluator ([`super::property::Evaluator`]) is stateless: every
4//! pending attempt re-runs every sequence search from its start over the whole
5//! window each time a position is appended ([`super::trace::attempt_word_calls`]
6//! is a tick-ordered BTreeSet search). That is `O(window)` per attempt per tick,
7//! and `O(window^2)` for a weak sequence, which re-runs the search once per cut.
8//!
9//! [`Accel`] keeps the search state of every `(sequence entry, start tick, local
10//! frame)` that has been asked about between calls, and advances it by the ticks
11//! that arrived since. The result is, by construction, what the reference
12//! returns; the engine's differential mode (see `engine.rs`) runs both and asserts
13//! it, and so does a property-level exhaustive test.
14//!
15//! # The model
16//!
17//! A search over a word `w = real samples ++ constant tail` is a set of
18//! *configurations* `(tick, state, frame)` closed under the automaton's edges.
19//! Everything that happens at a REAL tick (`tick < real`) depends on the samples
20//! only, so it is the same for every word and is advanced exactly once, forever.
21//! What differs between words is the tail: from tick `real` on every position is
22//! the letter `⊤` or `⊥` (a guard holds on `⊤` only, a value read there is `0`),
23//! so the search over the tail is a pure function of the *frontier* -- the
24//! configurations that are still alive at tick `real` -- and of the letter spec.
25//! The frontier is stored as a pool of [`Rng`]s ("`(state, frame)` is alive at
26//! every tick of `lo..=hi`", which is what a `##[m:n]` edge produces), canonical
27//! per `(state, frame)`, and the tail search is memoized by the frontier's
28//! signature relative to `real`: thousands of attempts that are in the same
29//! position share one tail search.
30//!
31//! # What it declines
32//!
33//! Anything the reference does that is not a function of the above is declined
34//! (the caller falls back to the reference, which is always correct): several
35//! clocks, subroutine calls, end points, `first_match`, aborts, a sequence leaf
36//! that is the `disable iff` condition (its value changes under a position), a
37//! query for an earlier word than one already advanced past, any error, and an
38//! interner that outgrew its cap.
39//!
40//! Where the reference can fail on a LETTER tick (only a `LocalVarStep` reads a
41//! variable there) the accelerator never skips a tail search the reference runs
42//! (`exact`): a skipped search could hide an error.
43use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
44use std::hash::{BuildHasherDefault, Hasher};
45
46use super::ir::{Automaton, LeafId, ThreadStateId, Transition};
47use super::property::{Prop, PropEnv, PropId, PropertyIr};
48use super::trace::{Frame, Matches, read_local};
49
50/// The end of an unbounded range.
51const INF: u64 = u64::MAX;
52/// Distinct local-variable frames the interner may hold before the accelerator
53/// switches itself off (and everything it knows is dropped).
54const MAX_FRAMES: usize = 1 << 20;
55/// Memoized tail searches before the memo is cleared.
56const MAX_MEMO: usize = 1 << 16;
57
58#[derive(Default, Clone, Copy)]
59struct Fx(u64);
60
61impl Fx {
62    #[inline]
63    fn add(&mut self, w: u64) {
64        self.0 = (self.0.rotate_left(5) ^ w).wrapping_mul(0x517c_c1b7_2722_0a95);
65    }
66}
67
68impl Hasher for Fx {
69    #[inline]
70    fn finish(&self) -> u64 {
71        self.0
72    }
73    #[inline]
74    fn write(&mut self, bytes: &[u8]) {
75        for &b in bytes {
76            self.add(u64::from(b));
77        }
78    }
79    #[inline]
80    fn write_u8(&mut self, i: u8) {
81        self.add(u64::from(i));
82    }
83    #[inline]
84    fn write_u32(&mut self, i: u32) {
85        self.add(u64::from(i));
86    }
87    #[inline]
88    fn write_u64(&mut self, i: u64) {
89        self.add(i);
90    }
91    #[inline]
92    fn write_i64(&mut self, i: i64) {
93        self.add(i as u64);
94    }
95    #[inline]
96    fn write_usize(&mut self, i: usize) {
97        self.add(i as u64);
98    }
99}
100
101/// The search states of a window, by start tick: each key is `(entry, start,
102/// frame id, weak)` of a [`Seq`]. Nearly every start has one, kept inline.
103type ByStart = BTreeMap<u64, smallvec::SmallVec<[(u32, u64, u32, bool); 1]>>;
104type FxMap<K, V> = HashMap<K, V, BuildHasherDefault<Fx>>;
105type FxSet<K> = HashSet<K, BuildHasherDefault<Fx>>;
106
107/// `(state, frame)` is alive at every tick of `lo..=hi`.
108#[derive(Clone, Copy, PartialEq, Eq, Debug)]
109struct Rng {
110    state: u32,
111    frame: u32,
112    lo: u64,
113    hi: u64,
114}
115
116impl Rng {
117    fn same_key(&self, o: &Rng) -> bool {
118        self.state == o.state && self.frame == o.frame
119    }
120
121    /// Whether the two ranges overlap or touch (so their union is one range).
122    fn joins(&self, o: &Rng) -> bool {
123        self.lo <= o.hi.saturating_add(1) && o.lo <= self.hi.saturating_add(1)
124    }
125}
126
127/// Adds `a` to a canonical pool: per `(state, frame)` the ranges stay disjoint
128/// and non-adjacent.
129fn add_range(pool: &mut Vec<Rng>, mut a: Rng) {
130    loop {
131        let hit = pool.iter().position(|e| e.same_key(&a) && e.joins(&a));
132        match hit {
133            Some(i) => {
134                let e = pool.swap_remove(i);
135                a.lo = a.lo.min(e.lo);
136                a.hi = a.hi.max(e.hi);
137            }
138            None => break,
139        }
140    }
141    pool.push(a);
142}
143
144/// The interner of local-variable frames: a frame is its id everywhere else.
145#[derive(Clone)]
146struct Frames {
147    list: Vec<Frame>,
148    ids: FxMap<Frame, u32>,
149    /// The id of the frame of zero slots.
150    empty: u32,
151}
152
153impl Frames {
154    fn new() -> Self {
155        let mut f = Frames {
156            list: Vec::new(),
157            ids: FxMap::default(),
158            empty: 0,
159        };
160        f.empty = f.intern_owned(Vec::new());
161        f
162    }
163
164    fn len(&self) -> usize {
165        self.list.len()
166    }
167
168    fn get(&self, id: u32) -> &Frame {
169        &self.list[id as usize]
170    }
171
172    fn intern(&mut self, f: &[Option<i64>]) -> u32 {
173        if f.is_empty() {
174            return self.empty;
175        }
176        if let Some(&id) = self.ids.get(f) {
177            return id;
178        }
179        self.intern_owned(f.to_vec())
180    }
181
182    fn intern_owned(&mut self, f: Frame) -> u32 {
183        if let Some(&id) = self.ids.get(&f) {
184            return id;
185        }
186        let id = self.list.len() as u32;
187        self.list.push(f.clone());
188        self.ids.insert(f, id);
189        id
190    }
191}
192
193/// The search state of one `(entry, start, frame)`.
194#[derive(Clone)]
195struct Seq {
196    /// Every tick below this has been processed.
197    processed: u64,
198    /// The configurations alive at `processed` and later.
199    pool: Vec<Rng>,
200    /// The matches that ended at a real tick below `processed`, by end tick
201    /// (ascending), each tick's frames sorted and distinct.
202    matches: Vec<(u64, u32)>,
203    /// A weak state: the first `n` such that `w^{start..n-1} ⊤^ω` has no strong
204    /// match, or [`INF`] while there is none below `processed`.
205    false_at: u64,
206    /// A weak state that has matched at a real tick: every cut holds from now on,
207    /// so the search stops.
208    done: bool,
209    /// An error was met: this state is never used again.
210    poisoned: bool,
211}
212
213/// What a tail search is asked for.
214#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
215enum Want {
216    /// Whether any match ends on the tail.
217    Exists,
218    /// Every match that ends on the tail.
219    All,
220    /// For a `⊥` tail, whether every longer cut (`⊥^k` then `⊤`, `1 <= k <=
221    /// span + 1`) of a weak sequence has a match.
222    ExtraCuts,
223    /// [`Want::ExtraCuts`] without the early exit at the first cut that fails:
224    /// what the reference runs when every cut has a match already (`exact`).
225    ExtraCutsAll,
226}
227
228/// The memo key of a tail search.
229#[derive(Clone, PartialEq, Eq, Hash)]
230struct MemoKey {
231    /// The frontier relative to the tail's first tick, sorted.
232    sig: Vec<(u32, u32, u64, u64)>,
233    /// How many `⊥` letters precede the tail proper.
234    k: u32,
235    top: bool,
236    want: Want,
237}
238
239#[derive(Clone)]
240enum Found {
241    Bool(bool),
242    /// `(offset from the tail's first tick, frame)`, ascending by offset.
243    List(Vec<(u32, u32)>),
244}
245
246/// The part of the accelerator that is not per-sequence state (kept apart so a
247/// search state can be borrowed from the map while this is used).
248#[derive(Clone)]
249struct Core {
250    frames: Frames,
251    memo: FxMap<MemoKey, Found>,
252    span: u32,
253    /// The automaton has a `LocalVarStep`, the one edge that can fail on a
254    /// letter tick: never skip a tail search the reference would run.
255    exact: bool,
256    stack: Vec<(u32, u32)>,
257    seen: FxSet<(u32, u32)>,
258    adds: Vec<Rng>,
259    hits: Vec<u32>,
260    /// The signature buffer [`Core::tail`] builds each memo key in; a memo hit
261    /// hands it back, so only a miss allocates.
262    sig_buf: Vec<(u32, u32, u64, u64)>,
263}
264
265/// The guard leaf of a one-letter Boolean sequence from `entry`: one edge
266/// `guard / Epsilon` into a state whose only edge is a `Match` (16.9.1: a lone
267/// Boolean expression as a sequence). Such a sequence matches at its start tick
268/// exactly when the leaf holds there, which needs no search state.
269fn one_letter_guard(au: &Automaton, entry: ThreadStateId) -> Option<LeafId> {
270    let [edge] = au.states[entry.0 as usize].edges.as_slice() else {
271        return None;
272    };
273    let guard = edge.guard?;
274    if !matches!(edge.label, Transition::Epsilon) {
275        return None;
276    }
277    let [end] = au.states[edge.target.0 as usize].edges.as_slice() else {
278        return None;
279    };
280    (end.guard.is_none() && matches!(end.label, Transition::Match { .. })).then_some(guard)
281}
282
283type Real<'a> = Option<(&'a dyn PropEnv, u64)>;
284
285impl Core {
286    /// Processes tick `t` over `pool` (every range with `lo == t` is alive now,
287    /// and consumed; the ranges the tick creates are merged in). Returns whether
288    /// a match ended on it, pushing `(t, frame)` per distinct matching frame
289    /// onto `out`. `real` is the environment of a real tick (`None`: the tick is
290    /// the letter `letter`).
291    fn process_tick(
292        &mut self,
293        au: &Automaton,
294        real: Real<'_>,
295        letter: Option<bool>,
296        t: u64,
297        pool: &mut Vec<Rng>,
298        out: &mut Vec<(u64, u32)>,
299    ) -> Result<bool, ()> {
300        self.stack.clear();
301        self.seen.clear();
302        self.adds.clear();
303        self.hits.clear();
304        for r in pool.iter_mut() {
305            debug_assert!(r.lo >= t);
306            if r.lo == t {
307                self.stack.push((r.state, r.frame));
308                r.lo += 1;
309            }
310        }
311        pool.retain(|r| r.lo <= r.hi);
312        while let Some((s, f)) = self.stack.pop() {
313            if !self.seen.insert((s, f)) {
314                continue;
315            }
316            for edge in &au.states[s as usize].edges {
317                if let Some(g) = edge.guard {
318                    let holds = match (letter, real) {
319                        (Some(top), _) => top,
320                        (None, Some((env, base))) => {
321                            env.value(g, (t - base) as u32, self.frames.get(f).as_slice())
322                                .map_err(|_| ())?
323                                != 0
324                        }
325                        (None, None) => return Err(()),
326                    };
327                    if !holds {
328                        continue;
329                    }
330                }
331                let target = edge.target.0;
332                match &edge.label {
333                    Transition::Epsilon => self.stack.push((target, f)),
334                    Transition::Delay { min, max } => {
335                        let hi = match max {
336                            Some(m) => t + u64::from(*m),
337                            None => INF,
338                        };
339                        if *min == 0 {
340                            self.stack.push((target, f));
341                            if hi > t {
342                                self.adds.push(Rng {
343                                    state: target,
344                                    frame: f,
345                                    lo: t + 1,
346                                    hi,
347                                });
348                            }
349                        } else {
350                            let lo = t + u64::from(*min);
351                            if lo <= hi {
352                                self.adds.push(Rng {
353                                    state: target,
354                                    frame: f,
355                                    lo,
356                                    hi,
357                                });
358                            }
359                        }
360                    }
361                    Transition::Match { .. } => self.hits.push(f),
362                    Transition::LocalVarWrite { var, value } => {
363                        let raw = match (letter, real) {
364                            (Some(_), _) => 0,
365                            (None, Some((env, base))) => env
366                                .value(*value, (t - base) as u32, self.frames.get(f).as_slice())
367                                .map_err(|_| ())?,
368                            (None, None) => return Err(()),
369                        };
370                        let v = au.local_var(*var).cast(raw);
371                        let mut f2 = self.frames.get(f).clone();
372                        f2[var.0 as usize] = Some(v);
373                        let id = self.frames.intern_owned(f2);
374                        self.stack.push((target, id));
375                    }
376                    Transition::LocalVarCopy { dst, src } => {
377                        let src_frame = self.frames.get(f);
378                        let mut f2 = src_frame.clone();
379                        // Wide variables copy limb by limb with extension/truncation
380                        // (`wide::copy_local`), the same code the reference trace runs.
381                        super::wide::copy_local(
382                            &mut f2,
383                            *dst,
384                            au.local_var(*dst),
385                            src_frame,
386                            *src,
387                            au.local_var(*src),
388                        );
389                        let id = self.frames.intern_owned(f2);
390                        self.stack.push((target, id));
391                    }
392                    Transition::LocalVarStep { var, delta } => {
393                        let decl = au.local_var(*var);
394                        let mut f2 = self.frames.get(f).clone();
395                        if decl.is_wide() {
396                            super::wide::step_local(&mut f2, *var, decl, *delta).ok_or(())?;
397                        } else {
398                            let cur = read_local(self.frames.get(f), *var).map_err(|_| ())?;
399                            f2[var.0 as usize] = Some(decl.cast(cur.wrapping_add(*delta)));
400                        }
401                        let id = self.frames.intern_owned(f2);
402                        self.stack.push((target, id));
403                    }
404                    // Eligibility excludes every other edge.
405                    _ => return Err(()),
406                }
407            }
408        }
409        // Drain, not `take`: the buffer (and its capacity) stays for the next tick.
410        let mut adds = std::mem::take(&mut self.adds);
411        for a in adds.drain(..) {
412            add_range(pool, a);
413        }
414        self.adds = adds;
415        let hit = !self.hits.is_empty();
416        if hit {
417            self.hits.sort_unstable();
418            self.hits.dedup();
419            out.extend(self.hits.iter().map(|&f| (t, f)));
420        }
421        Ok(hit)
422    }
423
424    /// The matches of a tail search from the frontier `pool` at tick `r`, over the
425    /// letters `⊥^k` then `top` forever, to the horizon `r + k + span + 1`.
426    /// `exists` stops at the first match.
427    fn tail_run(
428        &mut self,
429        au: &Automaton,
430        pool: &[Rng],
431        r: u64,
432        k: u32,
433        top: bool,
434        exists: bool,
435    ) -> Result<Found, ()> {
436        let h = r + u64::from(k) + u64::from(self.span) + 1;
437        let mut pool: Vec<Rng> = pool.iter().filter(|x| x.lo < h).copied().collect();
438        let mut found: Vec<(u64, u32)> = Vec::new();
439        let stop_early = exists && !self.exact;
440        let mut any = false;
441        let mut t = r;
442        while let Some(next) = pool.iter().map(|x| x.lo).min() {
443            t = t.max(next);
444            if t >= h {
445                break;
446            }
447            let letter = if t - r < u64::from(k) { false } else { top };
448            let hit = self.process_tick(au, None, Some(letter), t, &mut pool, &mut found)?;
449            if hit {
450                any = true;
451                if stop_early {
452                    return Ok(Found::Bool(true));
453                }
454            }
455            t += 1;
456        }
457        if exists {
458            return Ok(Found::Bool(any));
459        }
460        Ok(Found::List(
461            found
462                .into_iter()
463                .map(|(t, f)| ((t - r) as u32, f))
464                .collect(),
465        ))
466    }
467
468    /// [`Self::tail_run`], memoized by the frontier's signature.
469    fn tail(
470        &mut self,
471        au: &Automaton,
472        pool: &[Rng],
473        r: u64,
474        k: u32,
475        top: bool,
476        want: Want,
477    ) -> Result<Found, ()> {
478        if pool.is_empty() {
479            return Ok(match want {
480                Want::All => Found::List(Vec::new()),
481                Want::Exists => Found::Bool(false),
482                // No frontier: no cut has a match.
483                Want::ExtraCuts | Want::ExtraCutsAll => Found::Bool(false),
484            });
485        }
486        let h = r + u64::from(k) + u64::from(self.span) + 1;
487        let mut sig = std::mem::take(&mut self.sig_buf);
488        sig.clear();
489        sig.extend(
490            pool.iter()
491                .filter(|x| x.lo < h)
492                .map(|x| (x.state, x.frame, x.lo - r, x.hi.min(h - 1) - r)),
493        );
494        sig.sort_unstable();
495        let key = MemoKey { sig, k, top, want };
496        if let Some(v) = self.memo.get(&key) {
497            let v = v.clone();
498            self.sig_buf = key.sig;
499            return Ok(v);
500        }
501        let res = match want {
502            Want::Exists => self.tail_run(au, pool, r, k, top, true)?,
503            Want::All => self.tail_run(au, pool, r, k, top, false)?,
504            Want::ExtraCuts | Want::ExtraCutsAll => {
505                let mut all = true;
506                for kk in 1..=self.span + 1 {
507                    match self.tail_run(au, pool, r, kk, true, true)? {
508                        Found::Bool(true) => {}
509                        _ => {
510                            all = false;
511                            if want == Want::ExtraCuts {
512                                break;
513                            }
514                        }
515                    }
516                }
517                Found::Bool(all)
518            }
519        };
520        if self.memo.len() >= MAX_MEMO {
521            self.memo.clear();
522        }
523        self.memo.insert(key, res.clone());
524        Ok(res)
525    }
526
527    /// Advances a (non-weak) state to tick `to`.
528    fn advance(
529        &mut self,
530        au: &Automaton,
531        real: (&dyn PropEnv, u64),
532        seq: &mut Seq,
533        to: u64,
534    ) -> Result<(), ()> {
535        while seq.processed < to {
536            let Some(next) = seq.pool.iter().map(|x| x.lo).min() else {
537                seq.processed = to;
538                break;
539            };
540            if next > seq.processed {
541                seq.processed = next.min(to);
542                continue;
543            }
544            let t = seq.processed;
545            self.process_tick(au, Some(real), None, t, &mut seq.pool, &mut seq.matches)?;
546            seq.processed = t + 1;
547        }
548        Ok(())
549    }
550
551    /// Advances a weak state to tick `to`, stopping at the first cut that has no
552    /// match.
553    fn advance_weak(
554        &mut self,
555        au: &Automaton,
556        real: (&dyn PropEnv, u64),
557        seq: &mut Seq,
558        to: u64,
559    ) -> Result<(), ()> {
560        while seq.processed < to && seq.false_at == INF && !seq.done {
561            let t = seq.processed;
562            self.process_tick(au, Some(real), None, t, &mut seq.pool, &mut seq.matches)?;
563            seq.processed = t + 1;
564            let n = t + 1;
565            let has = !seq.matches.is_empty();
566            if has && !self.exact {
567                seq.done = true;
568                seq.pool.clear();
569                seq.processed = to;
570                break;
571            }
572            let ran = if seq.pool.is_empty() {
573                false
574            } else {
575                matches!(
576                    self.tail(au, &seq.pool, n, 0, true, Want::Exists)?,
577                    Found::Bool(true)
578                )
579            };
580            if !has && !ran {
581                seq.false_at = n;
582            }
583        }
584        Ok(())
585    }
586}
587
588/// The incremental sequence evaluator of one assertion.
589#[derive(Clone)]
590pub struct Accel {
591    /// The absolute index of the window's first position.
592    base: u64,
593    off: bool,
594    seqs: FxMap<(u32, u64, u32, bool), Seq>,
595    by_start: ByStart,
596    core: Core,
597    /// Per property node, how many ticks past a start its verdict can look (`None`:
598    /// unboundedly far), see [`reach_table`].
599    reach: Vec<Option<u32>>,
600    /// Per property node, the verdicts that no later sample can change, see
601    /// [`Fin`]. Empty when [`Self::memo_ok`] is not.
602    fins: Vec<Fin>,
603    /// Whether verdicts of one tick can be shared across attempts, see
604    /// [`Accel::new`].
605    memo_ok: bool,
606}
607
608/// The settled verdicts of one property node `p` (the operand of an `always`,
609/// `eventually` or `until`).
610///
611/// A verdict `sat(p, t)` of a node whose lookahead is `R` ticks reads only the
612/// samples `t..=t+R`; once `t + R` is a real tick of the window it is the same on
613/// every word, whatever the tail or the length, so it can be computed once and
614/// then every attempt that passes over `t` reads it back instead of evaluating
615/// `p` there again. The ticks are settled in order (`next` is the first that is
616/// not), and only the verdict the operators ask for is kept: the first FALSE tick
617/// at or after a start decides an `always`, the first TRUE one an `eventually`.
618#[derive(Clone, Default)]
619struct Fin {
620    /// Absolute ticks below `next` (and from the window's base) are settled.
621    next: u64,
622    /// The verdict of tick `next` is an error (it fails the same way every time
623    /// it is asked): nothing is settled past it.
624    blocked: bool,
625    keep_ones: bool,
626    keep_zeros: bool,
627    ones: BTreeSet<u64>,
628    zeros: BTreeSet<u64>,
629}
630
631impl Fin {
632    fn evict(&mut self, base: u64) {
633        if self.next < base {
634            self.next = base;
635            self.blocked = false;
636        }
637        if self.ones.first().is_some_and(|&t| t < base) {
638            self.ones = self.ones.split_off(&base);
639        }
640        if self.zeros.first().is_some_and(|&t| t < base) {
641            self.zeros = self.zeros.split_off(&base);
642        }
643    }
644}
645
646/// How many ticks past its start the longest simple path of the sequence entered
647/// at `entry` can span, plus one; `None` when the automaton has a cycle or an
648/// unbounded delay on a path from `entry` (a search then goes on as long as the
649/// word does).
650fn seq_reach(au: &Automaton, entry: u32) -> Option<u32> {
651    let n = au.states.len();
652    let mut color = vec![0u8; n];
653    let mut best = vec![0u32; n];
654    let mut stack = vec![(entry as usize, 0usize)];
655    *color.get_mut(entry as usize)? = 1;
656    while let Some(&(s, i)) = stack.last() {
657        let edges = &au.states[s].edges;
658        if let Some(e) = edges.get(i) {
659            stack.last_mut()?.1 += 1;
660            if matches!(e.label, Transition::Match { .. }) {
661                // A match ends the thread; its target is never entered.
662                continue;
663            }
664            let t = e.target.0 as usize;
665            match *color.get(t)? {
666                1 => return None,
667                0 => {
668                    color[t] = 1;
669                    stack.push((t, 0));
670                }
671                _ => {}
672            }
673        } else {
674            let mut longest = 0u32;
675            for e in edges {
676                let w = match e.label {
677                    Transition::Match { .. } => continue,
678                    Transition::Delay { min, max } => max?.max(min),
679                    _ => 0,
680                };
681                longest = longest.max(w.checked_add(best[e.target.0 as usize])?);
682            }
683            if longest > MAX_REACH {
684                return None;
685            }
686            best[s] = longest;
687            color[s] = 2;
688            stack.pop();
689        }
690    }
691    Some(best[entry as usize] + 1)
692}
693
694/// The most ticks past a start a verdict may look for the node to be memoized.
695const MAX_REACH: u32 = 1 << 16;
696
697/// The lookahead of every property node (`None`: unbounded, or not modelled): an
698/// upper bound on how far past its start tick `sat` of the node reads. The
699/// operators' own arithmetic is the reference evaluator's: a nonoverlapped
700/// implication's consequent starts one tick after the end point, `nexttime [n]`
701/// moves `n` ticks, `always`/`eventually [lo:hi]` reach `hi` ticks.
702fn reach_table(ir: &PropertyIr) -> Vec<Option<u32>> {
703    let au = &ir.automaton;
704    let mut seqs: FxMap<u32, Option<u32>> = FxMap::default();
705    let mut out: Vec<Option<u32>> = Vec::with_capacity(ir.props.len());
706    for prop in &ir.props {
707        let get = |out: &[Option<u32>], id: PropId| out.get(id.0 as usize).copied().flatten();
708        let mut seq = |entry: ThreadStateId| {
709            *seqs
710                .entry(entry.0)
711                .or_insert_with(|| seq_reach(au, entry.0))
712        };
713        let r = match prop {
714            Prop::Seq { seq: s, .. } => seq(s.entry),
715            Prop::Not(p) | Prop::Clocked { p, .. } => get(&out, *p),
716            Prop::And(a, b) | Prop::Or(a, b) => {
717                get(&out, *a).zip(get(&out, *b)).map(|(a, b)| a.max(b))
718            }
719            Prop::If { then, els, .. } => {
720                let e = els.map_or(Some(0), |e| get(&out, e));
721                get(&out, *then).zip(e).map(|(a, b)| a.max(b))
722            }
723            Prop::Case { arms, default, .. } => {
724                let mut r = default.map_or(Some(0), |d| get(&out, d));
725                for arm in arms {
726                    r = r.zip(get(&out, arm.body)).map(|(a, b)| a.max(b));
727                }
728                r
729            }
730            Prop::Implication {
731                ante,
732                cons,
733                overlapped,
734            } => seq(ante.entry)
735                .zip(get(&out, *cons))
736                .map(|(a, c)| a.saturating_add(c).saturating_add(u32::from(!*overlapped))),
737            Prop::Nexttime { p, n, .. } => get(&out, *p).map(|r| r.saturating_add(*n)),
738            Prop::Always { p, hi, .. } | Prop::Eventually { p, hi, .. } => {
739                hi.zip(get(&out, *p)).map(|(h, r)| h.saturating_add(r))
740            }
741            Prop::Until { .. } | Prop::Abort { .. } | Prop::Let { .. } | Prop::Call { .. } => None,
742        };
743        out.push(r.filter(|&r| r <= MAX_REACH));
744    }
745    out
746}
747
748/// The `Σ delay bound` bound on how many ticks a simple path of the automaton
749/// can span, as [`super::property::Evaluator`] computes it.
750pub fn automaton_span(au: &Automaton) -> u32 {
751    1 + au
752        .states
753        .iter()
754        .flat_map(|s| &s.edges)
755        .map(|e| match e.label {
756            Transition::Delay { min, max } => max.unwrap_or(min).max(1),
757            _ => 1,
758        })
759        .sum::<u32>()
760}
761
762impl Accel {
763    /// The accelerator for `ir`, or `None` when it has a shape the accelerator
764    /// does not model (see the module docs).
765    ///
766    /// `locals_read`: some leaf reads a local variable, which makes evaluating
767    /// it fallible (an unassigned read), so the accelerator then never skips work
768    /// the reference does (`exact`).
769    pub fn new(
770        ir: &PropertyIr,
771        n_clocks: usize,
772        has_calls: bool,
773        locals_read: bool,
774    ) -> Option<Accel> {
775        let au = &ir.automaton;
776        if n_clocks > 1
777            || has_calls
778            || !au.end_points.is_empty()
779            || au.first_match_groups != 0
780            || ir
781                .props
782                .iter()
783                .any(|p| matches!(p, Prop::Abort { .. } | Prop::Call { .. }))
784        {
785            return None;
786        }
787        let mut exact = locals_read;
788        let mut reads = |l: LeafId| Some(l) == ir.disable;
789        for e in au.states.iter().flat_map(|s| &s.edges) {
790            if e.guard.is_some_and(&mut reads) {
791                return None;
792            }
793            match &e.label {
794                Transition::Epsilon
795                | Transition::Delay { .. }
796                | Transition::Match { .. }
797                | Transition::LocalVarCopy { .. } => {}
798                Transition::LocalVarWrite { value, .. } => {
799                    if reads(*value) {
800                        return None;
801                    }
802                }
803                Transition::LocalVarStep { .. } => exact = true,
804                _ => return None,
805            }
806        }
807        // A settled verdict must not depend on anything that changes under a
808        // position: the `disable iff` condition's current value does (`If`/`Case`
809        // may read it as a plain leaf), and a local variable's value is a
810        // property of the thread, not of the tick (`exact`).
811        let reads_disable = ir.props.iter().any(|p| match p {
812            Prop::If { cond, .. } => reads(*cond),
813            Prop::Case { selector, arms, .. } => {
814                reads(*selector) || arms.iter().any(|a| a.labels.iter().any(|&l| reads(l)))
815            }
816            _ => false,
817        });
818        let memo_ok = !exact && !reads_disable;
819        let mut fins = Vec::new();
820        if memo_ok {
821            fins.resize_with(ir.props.len(), Fin::default);
822            for p in &ir.props {
823                match p {
824                    Prop::Always { p, .. } => fins[p.0 as usize].keep_zeros = true,
825                    Prop::Eventually { p, .. } => fins[p.0 as usize].keep_ones = true,
826                    Prop::Until { p, q, .. } => {
827                        fins[p.0 as usize].keep_zeros = true;
828                        fins[q.0 as usize].keep_ones = true;
829                    }
830                    _ => {}
831                }
832            }
833        }
834        Some(Accel {
835            reach: if memo_ok { reach_table(ir) } else { Vec::new() },
836            fins,
837            memo_ok,
838            base: 0,
839            off: false,
840            seqs: FxMap::default(),
841            by_start: BTreeMap::new(),
842            core: Core {
843                frames: Frames::new(),
844                memo: FxMap::default(),
845                span: automaton_span(au),
846                exact,
847                stack: Vec::new(),
848                seen: FxSet::default(),
849                adds: Vec::new(),
850                hits: Vec::new(),
851                sig_buf: Vec::new(),
852            },
853        })
854    }
855
856    /// The absolute index of the first position of the window the next queries
857    /// are over.
858    pub fn set_base(&mut self, base: u64) {
859        self.base = base;
860    }
861
862    /// Forgets the searches that started before absolute position `base`.
863    pub fn evict_before(&mut self, base: u64) {
864        for fin in &mut self.fins {
865            fin.evict(base);
866        }
867        if self.by_start.is_empty() || self.by_start.keys().next().is_none_or(|&s| s >= base) {
868            return;
869        }
870        // Pops the old starts one by one: no split-off map is built per trim.
871        while let Some(old) = self.by_start.first_entry()
872            && *old.key() < base
873        {
874            for key in old.remove() {
875                self.seqs.remove(&key);
876            }
877        }
878    }
879
880    /// The absolute index of the window's first position.
881    pub fn base(&self) -> u64 {
882        self.base
883    }
884
885    /// How many ticks past a start the verdict of `id` can look, when the verdicts
886    /// of `id` can be settled and shared across attempts (see [`Fin`]); `None`
887    /// when they cannot.
888    pub fn final_reach(&self, id: PropId) -> Option<u32> {
889        if self.off || !self.memo_ok {
890            return None;
891        }
892        self.reach.get(id.0 as usize).copied().flatten()
893    }
894
895    /// The first absolute tick whose verdict of `id` is not settled yet, and
896    /// whether settling stopped there (an error): the settled ticks are
897    /// `base..next`.
898    pub fn final_next(&mut self, id: PropId) -> (u64, bool) {
899        let base = self.base;
900        let fin = &mut self.fins[id.0 as usize];
901        if fin.next < base {
902            fin.next = base;
903            fin.blocked = false;
904        }
905        (fin.next, fin.blocked)
906    }
907
908    /// Settles tick `at` (which must be [`Self::final_next`]) of `id`: its verdict,
909    /// or `None` when evaluating it is an error.
910    pub fn final_record(&mut self, id: PropId, at: u64, verdict: Option<bool>) {
911        let fin = &mut self.fins[id.0 as usize];
912        debug_assert_eq!(fin.next, at);
913        match verdict {
914            Some(true) => {
915                if fin.keep_ones {
916                    fin.ones.insert(at);
917                }
918            }
919            Some(false) => {
920                if fin.keep_zeros {
921                    fin.zeros.insert(at);
922                }
923            }
924            None => {
925                fin.blocked = true;
926                return;
927            }
928        }
929        fin.next = at + 1;
930    }
931
932    /// The first settled tick of `id` at or after `from` whose verdict is `v`
933    /// (only the verdicts the operators ask for are kept: false for the operand
934    /// of an `always` or the left of an `until`, true for `eventually` and the
935    /// right of an `until`).
936    pub fn final_first(&self, id: PropId, v: bool, from: u64) -> Option<u64> {
937        let fin = &self.fins[id.0 as usize];
938        let set = if v { &fin.ones } else { &fin.zeros };
939        set.range(from..).next().copied()
940    }
941
942    fn disable(&mut self) {
943        self.off = true;
944        self.seqs.clear();
945        self.by_start.clear();
946        for fin in &mut self.fins {
947            fin.ones.clear();
948            fin.zeros.clear();
949        }
950        self.core.memo.clear();
951        self.core.frames = Frames::new();
952    }
953
954    /// Common checks: `(real tick (absolute), start tick (absolute), frame id)`.
955    fn prep(
956        &mut self,
957        env: &dyn PropEnv,
958        real: u32,
959        pos: u32,
960        frame: &Frame,
961    ) -> Option<(u64, u64, u32)> {
962        if self.off || pos > real || env.timeline().is_some() {
963            return None;
964        }
965        if self.core.frames.len() > MAX_FRAMES {
966            self.disable();
967            return None;
968        }
969        let fid = self.core.frames.intern(frame);
970        Some((self.base + u64::from(real), self.base + u64::from(pos), fid))
971    }
972
973    /// The search state of `(entry, start, fid)`, advanced to `r`; `None` when
974    /// it cannot be (poisoned, or already past `r`, or an error).
975    #[allow(clippy::too_many_arguments)]
976    fn state<'a>(
977        seqs: &'a mut FxMap<(u32, u64, u32, bool), Seq>,
978        by_start: &mut ByStart,
979        core: &mut Core,
980        au: &Automaton,
981        env: &dyn PropEnv,
982        base: u64,
983        key: (u32, u64, u32, bool),
984        r: u64,
985    ) -> Option<&'a mut Seq> {
986        let (entry, start, fid, weak) = key;
987        let seq = seqs.entry(key).or_insert_with(|| {
988            by_start.entry(start).or_default().push(key);
989            Seq {
990                processed: start,
991                pool: vec![Rng {
992                    state: entry,
993                    frame: fid,
994                    lo: start,
995                    hi: start,
996                }],
997                matches: Vec::new(),
998                false_at: INF,
999                done: false,
1000                poisoned: false,
1001            }
1002        });
1003        if seq.poisoned || seq.processed > r {
1004            return None;
1005        }
1006        let res = if weak {
1007            core.advance_weak(au, (env, base), seq, r)
1008        } else {
1009            core.advance(au, (env, base), seq, r)
1010        };
1011        if res.is_err() {
1012            seq.poisoned = true;
1013            return None;
1014        }
1015        Some(seq)
1016    }
1017
1018    /// Every nonempty match of the sequence starting at `entry` at position
1019    /// `pos` with `frame`, on the word of `real` samples and the tail `top`: what
1020    /// `Evaluator::matches` returns, ascending by `(end tick, frame)`.
1021    #[allow(clippy::too_many_arguments)]
1022    pub fn matches(
1023        &mut self,
1024        au: &Automaton,
1025        env: &dyn PropEnv,
1026        entry: ThreadStateId,
1027        real: u32,
1028        top: bool,
1029        pos: u32,
1030        frame: &Frame,
1031    ) -> Option<Matches> {
1032        let (r, start, fid) = self.prep(env, real, pos, frame)?;
1033        if start < r
1034            && let Some(g) = one_letter_guard(au, entry)
1035        {
1036            let held = env.value(g, pos, frame.as_slice()).ok()? != 0;
1037            let mut one = Matches::new();
1038            if held {
1039                one.push((pos, frame.clone()));
1040            }
1041            return Some(one);
1042        }
1043        let mut all: Vec<(u64, u32)>;
1044        if start == r {
1045            let pool = [Rng {
1046                state: entry.0,
1047                frame: fid,
1048                lo: r,
1049                hi: r,
1050            }];
1051            let found = self.core.tail(au, &pool, r, 0, top, Want::All).ok()?;
1052            let Found::List(l) = found else { return None };
1053            all = l.into_iter().map(|(o, f)| (r + u64::from(o), f)).collect();
1054        } else {
1055            let key = (entry.0, start, fid, false);
1056            let seq = Self::state(
1057                &mut self.seqs,
1058                &mut self.by_start,
1059                &mut self.core,
1060                au,
1061                env,
1062                self.base,
1063                key,
1064                r,
1065            )?;
1066            let found = self.core.tail(au, &seq.pool, r, 0, top, Want::All).ok()?;
1067            let Found::List(l) = found else { return None };
1068            all = seq.matches.clone();
1069            all.extend(l.into_iter().map(|(o, f)| (r + u64::from(o), f)));
1070        }
1071        let frames = &self.core.frames;
1072        all.sort_unstable_by(|a, b| {
1073            a.0.cmp(&b.0)
1074                .then_with(|| frames.get(a.1).cmp(frames.get(b.1)))
1075        });
1076        all.dedup();
1077        Some(
1078            all.into_iter()
1079                .map(|(e, f)| ((e - self.base) as u32, frames.get(f).clone()))
1080                .collect(),
1081        )
1082    }
1083
1084    /// Whether the sequence has a match at all (`strong(seq)`).
1085    #[allow(clippy::too_many_arguments)]
1086    pub fn strong(
1087        &mut self,
1088        au: &Automaton,
1089        env: &dyn PropEnv,
1090        entry: ThreadStateId,
1091        real: u32,
1092        top: bool,
1093        pos: u32,
1094        frame: &Frame,
1095    ) -> Option<bool> {
1096        let (r, start, fid) = self.prep(env, real, pos, frame)?;
1097        if start < r
1098            && let Some(g) = one_letter_guard(au, entry)
1099        {
1100            return Some(env.value(g, pos, frame.as_slice()).ok()? != 0);
1101        }
1102        if start == r {
1103            let pool = [Rng {
1104                state: entry.0,
1105                frame: fid,
1106                lo: r,
1107                hi: r,
1108            }];
1109            return match self.core.tail(au, &pool, r, 0, top, Want::Exists).ok()? {
1110                Found::Bool(b) => Some(b),
1111                Found::List(_) => None,
1112            };
1113        }
1114        let key = (entry.0, start, fid, false);
1115        let seq = Self::state(
1116            &mut self.seqs,
1117            &mut self.by_start,
1118            &mut self.core,
1119            au,
1120            env,
1121            self.base,
1122            key,
1123            r,
1124        )?;
1125        let has = !seq.matches.is_empty();
1126        if has && !self.core.exact {
1127            return Some(true);
1128        }
1129        match self
1130            .core
1131            .tail(au, &seq.pool, r, 0, top, Want::Exists)
1132            .ok()?
1133        {
1134            Found::Bool(b) => Some(has || b),
1135            Found::List(_) => None,
1136        }
1137    }
1138
1139    /// `weak(seq)` from `pos` (F.5.3.1): every cut of the word, completed with
1140    /// `⊤`, has a match.
1141    #[allow(clippy::too_many_arguments)]
1142    pub fn weak(
1143        &mut self,
1144        au: &Automaton,
1145        env: &dyn PropEnv,
1146        entry: ThreadStateId,
1147        real: u32,
1148        top: bool,
1149        pos: u32,
1150        frame: &Frame,
1151    ) -> Option<bool> {
1152        let (r, start, fid) = self.prep(env, real, pos, frame)?;
1153        if start == r {
1154            let pool = [Rng {
1155                state: entry.0,
1156                frame: fid,
1157                lo: r,
1158                hi: r,
1159            }];
1160            let want = if top { Want::Exists } else { Want::ExtraCuts };
1161            return match self.core.tail(au, &pool, r, 0, true, want).ok()? {
1162                Found::Bool(b) => Some(b),
1163                Found::List(_) => None,
1164            };
1165        }
1166        let key = (entry.0, start, fid, true);
1167        let seq = Self::state(
1168            &mut self.seqs,
1169            &mut self.by_start,
1170            &mut self.core,
1171            au,
1172            env,
1173            self.base,
1174            key,
1175            r,
1176        )?;
1177        if seq.false_at <= r {
1178            return Some(false);
1179        }
1180        if top {
1181            return Some(true);
1182        }
1183        let has = !seq.matches.is_empty() || seq.done;
1184        if has && !self.core.exact {
1185            return Some(true);
1186        }
1187        // Every cut has the real match, so the reference runs them all.
1188        let want = if has {
1189            Want::ExtraCutsAll
1190        } else {
1191            Want::ExtraCuts
1192        };
1193        match self.core.tail(au, &seq.pool, r, 0, true, want).ok()? {
1194            Found::Bool(b) => Some(has || b),
1195            Found::List(_) => None,
1196        }
1197    }
1198}