Skip to main content

vj_sim/sva/
ir.rs

1//! T4 (`docs/superpowers/plans/2026-10-03-sva.md`): the automaton IR
2//! skeleton -- plan §1.2's "a dedicated, interpreted automaton per
3//! assertion, compiled once at elaboration time into a static transition
4//! table (`vj-sim::sva::Automaton`), stepped by a small engine in
5//! `vj-runtime::sva_engine`" decision, materialized as the types that
6//! table is made of. This task builds the IR *types only* -- no builder
7//! (that's T9/T10 onward, consuming `vj-codegen::sva::{ranges, op}`'s
8//! already-recovered delay/repetition bounds and operator values) and no
9//! engine (`vj-runtime::sva_engine`, a later task).
10//!
11//! Every ID below is a handle into whichever table its automaton (or the
12//! engine stepping it) owns -- the same "small newtype handle, real storage
13//! lives in a `slotmap`/`Vec` elsewhere" shape this crate already uses for
14//! [`crate::netlist::InstanceId`]/[`crate::process::StmtId`]/[`crate::analysis::RegionId`]
15//! and every other cross-module ID.
16#![allow(dead_code)]
17
18/// Identifies one compiled automaton -- the whole static transition table
19/// for one concurrent assertion's property/sequence tree (16.12-16.13).
20/// One per `AnalyzedAssertion` (`vj-codegen::sva`'s frontend-facing
21/// sibling queries that type at elaboration time); a later task's builder
22/// mints these as it compiles each assertion's tree.
23#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
24pub struct AutomatonId(pub u32);
25
26/// Identifies one state inside a specific [`AutomatonId`]'s transition
27/// table. Scoped to its automaton (not globally unique on its own, the same
28/// way `BlockId`/`SegId` are scoped to one process's CFG) -- a thread
29/// record (not yet defined; owned by the engine, not this IR) pairs this
30/// with its `AutomatonId` to name exactly where in exactly which automaton
31/// a live thread is.
32#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
33pub struct ThreadStateId(pub u32);
34
35/// Identifies one compiled Boolean leaf (16.6) by its position in whichever
36/// leaf table its automaton carries. T5's `vj-codegen::sva::leaf`
37/// compiles each leaf to a `PredicateFn` (D8's `compile_iff_guard`
38/// convention, reused verbatim per plan §1.2); this ID is how a
39/// [`Transition`] names *which* compiled leaf to invoke, without this crate
40/// (which has no `sv-lang`/codegen dependency, module doc comment) needing
41/// to know anything about how leaves are compiled.
42#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
43pub struct LeafId(pub u32);
44
45/// Identifies one clock of an assertion (16.13, sva T28): the clocking events the
46/// builder met, interned by structural equality, so two `@(posedge clk)` written
47/// in different places are ONE clock and `@(posedge clk)` / `@(negedge clk)` are
48/// two. `ClockId(0)` is the clock the assertion is clocked by (the event its
49/// statement or the first clocking event met names): the clock every thread and
50/// property starts in, and the only one a single-clock assertion has.
51///
52/// A [`Transition::Delay`] counts ticks of the clock its thread is in, and
53/// [`Transition::ClockSync`] moves the thread to another one; which instants ARE
54/// ticks of each clock is a fact about one simulation, handed to the interpreters
55/// as a [`crate::sva::trace::Timeline`] (none: every instant is a tick of every
56/// clock, the single-clock case).
57#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
58pub struct ClockId(pub u32);
59
60/// Identifies one scheduled action -- a pass/fail/vacuous-pass report
61/// (16.14.1-16.14.4, 17.7.3), a sequence/property coverage increment
62/// (16.14.3), or a checker action block -- a later task (T29 onward, per
63/// plan §1.4's generalized `MaturationQueue<ReportKind>`) defines the
64/// payload each one carries; a [`Transition::Match`] names only *which*
65/// actions fire, not what they do.
66#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
67pub struct ActionId(pub u32);
68
69/// Identifies one local-variable slot (16.10's assertion-variable
70/// declarations and `local` formal arguments, 16.11's match-item writes) in a
71/// thread's per-thread local variable frame. The frame is one slot per id,
72/// `0..`[`Automaton::local_vars`]`.len()`, each either unassigned or holding a
73/// value of the slot's [`LocalVarDecl`] type; a thread that forks (an `or`
74/// branch, a delay window's several end ticks) takes its own COPY of the frame,
75/// which is what gives every match thread its own latest assignments (16.10
76/// rule (c)). T13 defines the slots and the edges that read/write/copy them;
77/// the runtime heap handle that stores a live frame is the engine's (T29), so
78/// here a slot is only the name a [`Transition::LocalVarWrite`]/
79/// [`Transition::LocalVarCopy`] and a compiled leaf's read refer to.
80#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
81pub struct LocalVarId(pub u32);
82
83/// The declared type of one local-variable slot: every value stored in the slot
84/// is first cast to it (16.10 "the value of a local variable is a value of its
85/// declared type"; 16.8.2 "cast to the type of" for an input/inout formal's
86/// initial value and for the value written back to an output/inout actual). The
87/// builder derives it from the variable's declared integral type; a non-integral
88/// local variable is a loud build error, never a slot.
89#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
90pub struct LocalVarDecl {
91    /// Width in bits (`>= 1`).
92    pub width: u32,
93    /// Whether the type is signed: a signed value is sign-extended from its top
94    /// bit after truncation, an unsigned one zero-extended.
95    pub signed: bool,
96}
97
98impl LocalVarDecl {
99    /// Whether the variable is wider than one frame slot (`> 64` bits) and so
100    /// occupies [`slots`](Self::slots) consecutive frame slots, see
101    /// [`Automaton::new_local_var`].
102    pub fn is_wide(&self) -> bool {
103        self.width > 64
104    }
105
106    /// How many 64-bit limbs one plane (value or unknown) of the variable has:
107    /// `ceil(width / 64)`, and `1` for a narrow variable.
108    pub fn limbs(&self) -> usize {
109        (self.width as usize).div_ceil(64).max(1)
110    }
111
112    /// How many consecutive frame slots the variable occupies: `1` when it fits
113    /// an `i64`, else `2 * limbs()` -- the value limbs (least significant
114    /// first), then the unknown-plane limbs (`x`/`z` bits, same order). A wide
115    /// variable is four-state; a narrow one is the two-state `i64` it always was.
116    pub fn slots(&self) -> usize {
117        if self.is_wide() { 2 * self.limbs() } else { 1 }
118    }
119
120    /// `value` fitted to this type: truncated to [`width`](Self::width) bits,
121    /// then sign-extended when [`signed`](Self::signed). A slot holds an `i64`,
122    /// so a type of 64 bits or more is the identity here: a wider variable is
123    /// stored limb by limb in several slots ([`slots`](Self::slots), the
124    /// helpers of [`crate::sva::wide`] fit it as a whole) and each limb slot is
125    /// an ordinary 64-bit unsigned one.
126    pub fn cast(&self, value: i64) -> i64 {
127        if self.width == 0 || self.width >= 64 {
128            return value;
129        }
130        let shift = 64 - self.width;
131        if self.signed {
132            (value << shift) >> shift
133        } else {
134            ((value as u64) << shift >> shift) as i64
135        }
136    }
137}
138
139/// One edge out of a [`ThreadStateId`] in a compiled automaton's static
140/// transition table (plan §1.2/§1.4). This is the whole vocabulary a
141/// sequence/property tree compiles down to -- every LRM operator (16.7's
142/// concatenation, 16.9's `and`/`or`/`intersect`/`throughout`/`within`/
143/// `first_match`, 16.12's property connectives, 17's checker actions) is
144/// *built from* these shapes by a later task's builder, not added as one of
145/// their own (T11's `first_match` markers are the one exception: "earliest
146/// match" is a property of a whole evaluation attempt, which no path-local
147/// edge can express). Matched exhaustively wherever it is consumed (the first such
148/// site, [`Transition::kind_name`] below, exists precisely so a future
149/// variant added here without updating every consumer fails to compile,
150/// not silently mis-steps at runtime -- ARCHITECTURE §7 item 9's "every
151/// kind match shouts" discipline, applied to a workspace-local enum
152/// instead of a mirrored slang one).
153#[derive(Clone, PartialEq, Eq, Hash, Debug)]
154pub enum Transition {
155    /// A `##N`/`##[m:n]` cycle delay (16.7) or a `[*n:m]`/`[->n:m]`/
156    /// `[=n:m]` repetition unrolled into a run of delay edges (16.9.2):
157    /// this edge is only eligible once at least `min` ticks of the clock the
158    /// thread is currently in (see [`ClockId`]) have
159    /// elapsed since the automaton entered the state it leaves, and must
160    /// be taken by `max` ticks (`None` = unbounded, the `$` form) or the
161    /// thread never gets another chance to -- the engine (T9/T10) is the
162    /// one enforcing that window, not this IR.
163    Delay { min: u32, max: Option<u32> },
164    /// A same-tick edge: zero elapsed ticks, consumed instantly. Used
165    /// unconditionally (a sequence concatenation boundary with a zero-width
166    /// operand, 16.9.2.1's empty-match rules, or any other place the
167    /// builder needs to join two states with no delay and no condition) --
168    /// but also, as of T9, paired with [`Edge::guard`] to model 16.9.1's
169    /// Boolean-leaf point-sample itself ("the expression is evaluated...
170    /// over 1 clock tick", which costs no ADDITIONAL elapsed time relative
171    /// to the clock tick it is sampled on -- the thing that advances time
172    /// in a sequence is the `##`/repetition operator *between* samples, not
173    /// a sample's own evaluation): see `Edge`'s own doc comment in
174    /// `vj-sim::sva::ir` for why.
175    Epsilon,
176    /// `disable iff`/property abort (16.12.14's `accept_on`/`reject_on`/
177    /// `sync_accept_on`/`sync_reject_on`, and 16.6/16.15's plain `disable
178    /// iff`): evaluated every simulation time step against current
179    /// (unsampled) values per 16.6, not gated on this edge's own clock --
180    /// the first tick `leaf` is true, the thread's run is decided per
181    /// `mode` without waiting for (or caring about) this edge's own
182    /// source/target states.
183    Abort { leaf: LeafId, mode: AbortMode },
184    /// This thread has reached its automaton's accept state: schedule
185    /// `actions` into the Reactive-region maturation queue (plan §1.4's
186    /// generalized `MaturationQueue<ReportKind>`, T2) rather than firing
187    /// them inline -- matches never fire synchronously with the Observed
188    /// step that discovers them (16.4.1-style maturation, generalized).
189    Match { actions: Vec<ActionId> },
190    /// A sequence match-item write (16.10/16.11): compute `value` (a
191    /// compiled leaf, reused verbatim -- 16.11 restricts match-item
192    /// expressions to the same side-effect-free leaf shape 16.6 does,
193    /// T5's validation pass), cast it to `var`'s declared type
194    /// ([`LocalVarDecl::cast`]) and store it into `var`'s slot of the thread's
195    /// frame before taking this edge. A compound assignment (`v += e`) is the
196    /// same edge: its `value` leaf is the whole `v + e`, which reads `v`'s own
197    /// slot (T13).
198    LocalVarWrite { var: LocalVarId, value: LeafId },
199    /// A same-tick slot-to-slot copy (T13): `dst` takes `src`'s value, cast to
200    /// `dst`'s declared type, or becomes UNASSIGNED when `src` is. The edge
201    /// exists for the two places 16.8.2/16.9 need a value to move between
202    /// threads' views of a variable without a leaf to compute it:
203    ///
204    /// - an `inout`/`output` `local` formal argument, whose value is written back
205    ///   to the actual's local variable when the instance's match completes
206    ///   (16.8.2);
207    /// - the branching operators `and`/`intersect` (and their `throughout`/
208    ///   `within` rewrites), whose second operand works on a SHADOW copy of every
209    ///   variable the first operand touches, copied in before the operands start
210    ///   and copied out afterwards for the variables that flow out only of the
211    ///   second operand (16.10, F.5.4). The two operands of the lockstep product
212    ///   share one frame, so without the shadow one operand's write would be
213    ///   visible to the other.
214    LocalVarCopy { dst: LocalVarId, src: LocalVarId },
215    /// Start of a `first_match(R)` region (16.9.3, T11): zero elapsed ticks,
216    /// like [`Epsilon`](Transition::Epsilon), but the thread taking it is
217    /// stamped with `(group, current tick)` -- the tick `R` begins on.
218    ///
219    /// `first_match(R)` keeps only `R`'s matches that END EARLIEST for a
220    /// given beginning (Annex F.5.2: `w |= first_match(R)` iff `w |= R` and no
221    /// proper prefix of `w` satisfies `R`). Whether a match is the earliest
222    /// is a fact about EVERY thread of the region, not about one path, so it
223    /// is decided against an attempt-wide table the engine keeps, keyed
224    /// `(group, begin tick)` -- see [`FirstMatchEnd`](Transition::FirstMatchEnd).
225    /// Nested and repeated regions carry one stamp each (every `first_match`
226    /// in a tree has its own [`FirstMatchId`], so stamps never collide).
227    FirstMatchBegin { group: FirstMatchId },
228    /// End of a `first_match(R)` region: a thread reaching this edge has just
229    /// produced a match of `R` that ended on the current tick. The engine
230    /// looks `(group, the tick this thread's `group` stamp holds)` up in the
231    /// attempt's first-match table:
232    ///
233    /// - absent: record the current tick as the earliest end, and take the
234    ///   edge;
235    /// - present and equal to the current tick: take the edge (several
236    ///   matches may end on that same earliest tick, and all of them are
237    ///   matches of `first_match(R)`);
238    /// - present and EARLIER: the thread dies (a shorter match of `R`, from
239    ///   the same beginning, already exists).
240    ///
241    /// The entry is recorded whether or not the thread's continuation later
242    /// succeeds: `first_match(R) ##1 c` discards the longer `R` matches even
243    /// when the earliest one is followed by `!c`. Threads are stepped in tick
244    /// order, so an earlier end tick is always seen before a later one.
245    /// Taking this edge also drops the thread's `group` stamp.
246    FirstMatchEnd { group: FirstMatchId },
247    /// A sequence match-item increment/decrement (16.11, T14): `var` becomes
248    /// `var + delta` (`delta` is `+1` for `x++`/`++x`, `-1` for `x--`/`--x`),
249    /// cast to `var`'s declared type. It is a dedicated edge rather than a
250    /// [`LocalVarWrite`](Transition::LocalVarWrite) because the new value is a
251    /// function of the variable alone: no compiled leaf is needed to compute
252    /// it. A match item's value is never observed (the statement form of
253    /// `x++` discards it), so pre- and post-forms are the same edge.
254    ///
255    /// The variable must hold a value (16.10: an inc/dec reads it); the
256    /// builder rejects every proof-of-unassigned and the interpreter fails the
257    /// attempt with `UnassignedRead` for one it could not.
258    LocalVarStep { var: LocalVarId, delta: i64 },
259    /// A sequence match-item subroutine call (16.11, T14): at every nonempty
260    /// end point of the sequence it is attached to, task or void function
261    /// `call` is scheduled in the Reactive region (fire-and-forget: assertion
262    /// evaluation neither waits on it nor receives anything back, and it runs
263    /// even when a sequence this one is part of later fails). `args` is one
264    /// entry per actual argument, in the formal order.
265    ///
266    /// The edge is a same-tick, unguarded pass-through for the thread; its
267    /// only effect is the call event the engine (T29) schedules. By-value
268    /// arguments are the SAMPLED value of their leaf under the thread's frame
269    /// (consistent with the match that produced it); by-reference arguments
270    /// are the actual itself, bound at call time. See [`CallArg`].
271    SubroutineCall { call: CallId, args: Vec<CallArg> },
272    /// The end point of another sequence instance (16.9.11, 16.13.5, 16.13.6): the
273    /// value of `inst.triggered` / `inst.matched` for the instance
274    /// [`Automaton::end_points`]`[point]`.
275    ///
276    /// The instance is evaluated INDEPENDENTLY of the thread that tests it: its
277    /// automaton runs free, one attempt from every tick of its clock ("the result
278    /// ... does not depend upon the starting point of the match of its operand
279    /// sequence"), and the instance has reached its end point on a tick when some
280    /// attempt has a NONEMPTY match ending there (16.9.11: an empty match never
281    /// activates the method). The runtime sets that status in the Observed region
282    /// and it persists for the rest of the time step, so a thread reads a status
283    /// that already includes the tick it is on. The edge takes no time.
284    ///
285    /// - `flag == None`: the edge is a guard. A thread takes it only when the
286    ///   instance reached its end point, and forks once per DISTINCT frame an
287    ///   attempt matched with (16.10: "the thread evaluating the instance to which
288    ///   triggered is applied will fork"), copying the matching frame's
289    ///   [`EndPoint::flow_out`] slots into its own. This is the application of the
290    ///   method as a whole maximal Boolean expression, the only one a local
291    ///   variable flows out of.
292    /// - `flag == Some(slot)`: the application sits inside a larger expression
293    ///   (`!s.triggered`, `a && s.triggered`), so nothing flows out. The edge is
294    ///   not a guard: it stores `1`/`0` (the instance did / did not reach its end
295    ///   point on this tick) into the thread's frame slot `flag`, and the leaf that
296    ///   holds the expression reads the slot where the application stands (a leaf
297    ///   reads its flags in the pre-order of the calls inside it, see the builder's
298    ///   `LeafLocals::end_flags`).
299    ///
300    /// For [`EndMethod::Matched`] the status is the instance's own clock's, held
301    /// until the first tick of THIS automaton's clock at or after the match: see
302    /// [`EndMethod`].
303    EndPoint {
304        point: EndPointId,
305        flag: Option<LocalVarId>,
306    },
307    /// The clock change of a multiclocked sequence (16.13.1, sva T28): `s1 ##1 s2`
308    /// (`strict`) or `s1 ##0 s2` (not `strict`) where `s1` ends in one clock and
309    /// `s2` begins in another. The thread leaves the clock it is in and takes
310    /// `to`:
311    ///
312    /// - `strict` (`##1`): "the nearest strictly subsequent tick" of `to` -- the
313    ///   first tick of `to` AFTER the instant the thread is at, even when the old
314    ///   clock's tick is also one of `to`'s;
315    /// - not `strict` (`##0`): "the nearest possibly overlapping tick": the first
316    ///   tick of `to` AT OR AFTER that instant, so no time passes when the two
317    ///   clocks tick together.
318    ///
319    /// A fused edge pair would need the single-clock meaning of `Delay{1,1}` to
320    /// be "one instant later", which only holds when every instant is a tick;
321    /// this edge is that statement made for the clocks involved. With no
322    /// timeline (every instant a tick of every clock) it is a `Delay{1,1}` /
323    /// `Epsilon`. The sequences either side must not admit the empty match
324    /// (16.13.1: "a maximal singly clocked subsequence ... shall admit only
325    /// nonempty matches"), which the builder checks.
326    ClockSync { to: ClockId, strict: bool },
327}
328
329/// Identifies one sequence instance whose end point an automaton tests, in its
330/// [`Automaton::end_points`] table. Scoped to its automaton, like
331/// [`ThreadStateId`].
332#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
333pub struct EndPointId(pub u32);
334
335/// Which end-point method a [`Transition::EndPoint`] applies.
336#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
337pub enum EndMethod {
338    /// `.triggered` (16.9.11, 16.13.6): the instance's ending clock is the clock
339    /// of the context the method appears in (the builder rejects any other), so
340    /// the status is read on the same tick it is set.
341    Triggered,
342    /// `.matched` (16.13.5): the instance may run on another clock. Its status is
343    /// SET at the instance's own end point and persists until the Observed region
344    /// following the first tick of the testing clock after the match, so a
345    /// testing tick sees every match since the previous testing tick, up to and
346    /// including a match at the very time of the tick (a one-slot handoff
347    /// between the two clock domains).
348    Matched,
349}
350
351/// One sequence instance whose end point an automaton tests (see
352/// [`Transition::EndPoint`]).
353#[derive(Clone, Debug, PartialEq, Eq)]
354pub struct EndPoint {
355    pub method: EndMethod,
356    /// Which end-point application of the whole assertion this is, numbered by the
357    /// builder across ALL nesting levels (the instance of an end point may itself
358    /// test others), so a site keeps one identity however deep it sits: the key of
359    /// the builder's per-site table (the instance expression and the clock it ends
360    /// on, which the engine needs and this IR does not carry).
361    pub site: u32,
362    /// The instance's own automaton: built with NO local variable flowing in
363    /// (16.10: a formal the instance reads must be assigned in the instance
364    /// first), so every attempt starts with an all-unassigned frame. Its
365    /// `local_vars` begin with the enclosing automaton's, so a slot id means the
366    /// same variable in both.
367    pub automaton: Automaton,
368    /// `(slot in the instance's frame, slot in the testing thread's frame)` for
369    /// every local variable that flows out of the maximal application of the
370    /// method (see [`Transition::EndPoint`]); empty for the flag form.
371    pub flow_out: Vec<(LocalVarId, LocalVarId)>,
372}
373
374/// Identifies one subroutine call site attached as a match item, in its
375/// automaton's call table (a table the builder keeps next to the leaf table:
376/// this IR knows only the handle, the runtime engine (T29) resolves it to the
377/// task or void function to invoke).
378#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
379pub struct CallId(pub u32);
380
381/// One actual argument of a [`Transition::SubroutineCall`] (16.11).
382#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
383pub enum CallArg {
384    /// A by-value (`input`) argument: the leaf's value, sampled at the end
385    /// point. Every actual that mentions a local variable is by value
386    /// (16.11: "a local variable cannot be passed by reference").
387    Value(LeafId),
388    /// A `ref`/`const ref` argument: the actual is an lvalue of the enclosing
389    /// scope, bound to the formal at call time with no copy. The engine
390    /// resolves the lvalue from the call table.
391    Ref,
392    /// A by-value argument that depends on nothing sampled and so has no leaf:
393    /// a string literal or a constant of a type a leaf cannot carry (a real, a
394    /// string). The kernel that runs the call lowers the actual itself; the
395    /// event carries no value for it (`None`).
396    Const,
397}
398
399/// Identifies one `first_match` region (16.9.3) inside its automaton: the key
400/// the engine's first-match table is indexed by, together with the tick the
401/// region began on. Minted by [`Automaton::new_first_match_group`]; every
402/// `first_match` operator in an assertion (and every copy of it a repetition
403/// unrolls into) has its own.
404#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
405pub struct FirstMatchId(pub u32);
406
407impl Transition {
408    /// A stable, short name per variant -- this enum's first exhaustive
409    /// match site (module doc comment: no real consumer exists yet, so
410    /// this is what keeps the "no wildcard arm" discipline load-bearing
411    /// today instead of unchecked).
412    pub fn kind_name(&self) -> &'static str {
413        match self {
414            Transition::Delay { .. } => "Delay",
415            Transition::Epsilon => "Epsilon",
416            Transition::Abort { .. } => "Abort",
417            Transition::Match { .. } => "Match",
418            Transition::LocalVarWrite { .. } => "LocalVarWrite",
419            Transition::LocalVarCopy { .. } => "LocalVarCopy",
420            Transition::FirstMatchBegin { .. } => "FirstMatchBegin",
421            Transition::FirstMatchEnd { .. } => "FirstMatchEnd",
422            Transition::LocalVarStep { .. } => "LocalVarStep",
423            Transition::SubroutineCall { .. } => "SubroutineCall",
424            Transition::EndPoint { .. } => "EndPoint",
425            Transition::ClockSync { .. } => "ClockSync",
426        }
427    }
428}
429
430/// The four abort/accept actions 16.12.14 defines, as a cross product of
431/// *direction* (does the thread succeed or fail when the condition fires)
432/// and *synchronization* (does the abort happen as soon as the condition
433/// is true mid-cycle ("asynchronous"), or only at the next clock tick
434/// ("synchronous") -- the LRM's own `isSync` flag, `AbortAssertionExpr`
435/// in `sv-lang-sys`'s vendored `AssertionExpr.h`, module doc comment of
436/// `vj-codegen::sva::op`):
437///
438/// | | asynchronous | synchronous |
439/// |---|---|---|
440/// | accept | `accept_on` -> [`Accept`](AbortMode::Accept) | `sync_accept_on` -> [`SyncAccept`](AbortMode::SyncAccept) |
441/// | reject | `reject_on` -> [`Reject`](AbortMode::Reject) | `sync_reject_on` -> [`SyncReject`](AbortMode::SyncReject) |
442#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
443pub enum AbortMode {
444    Accept,
445    Reject,
446    SyncAccept,
447    SyncReject,
448}
449
450impl AbortMode {
451    /// The number of variants (4, the full 16.12.14 cross product above).
452    pub const COUNT: usize = 4;
453    /// Every variant.
454    pub const ALL: [AbortMode; Self::COUNT] = [
455        AbortMode::Accept,
456        AbortMode::Reject,
457        AbortMode::SyncAccept,
458        AbortMode::SyncReject,
459    ];
460
461    /// `true` for the `sync_*_on` forms (16.12.14: the abort is deferred to
462    /// the next clock tick rather than applied as soon as the condition is
463    /// true mid-cycle).
464    pub fn is_sync(&self) -> bool {
465        matches!(self, AbortMode::SyncAccept | AbortMode::SyncReject)
466    }
467
468    /// `true` if a true condition makes the thread succeed (`accept_on`/
469    /// `sync_accept_on`); `false` if it makes the thread fail (`reject_on`/
470    /// `sync_reject_on`).
471    pub fn accepts(&self) -> bool {
472        matches!(self, AbortMode::Accept | AbortMode::SyncAccept)
473    }
474}
475
476/// One edge out of a [`ThreadStateId`] in a compiled [`Automaton`]: the
477/// condition/action it carries ([`Transition`]) plus the state it leads to.
478///
479/// T9 (`docs/superpowers/plans/2026-10-03-sva.md`) adds this type (and
480/// [`State`]/[`Automaton`] below) -- T4's own skeleton defined
481/// [`Transition`]'s five label shapes but, by its own module doc comment
482/// ("no lowering yet"), never wired a `target` state onto any of them, so
483/// there was no way yet to connect two states into an actual graph. T9 is
484/// the first task that builds a real automaton, so it is the one that
485/// completes the IR with the missing graph structure.
486///
487/// **`guard`, a deliberate, documented addition beyond the plan's literal
488/// text.** Plan §1.2 says a Boolean leaf (16.6) is evaluated by the engine
489/// and "the automaton engine calls these compiled leaves" -- but it never
490/// says *which* edge a leaf's truth value gates, and [`Transition`] itself
491/// has no `LeafId` field on its `Delay`/`Epsilon`/`Match` variants (only
492/// `Abort`/`LocalVarWrite` carry one, and for a different purpose: an abort
493/// condition and a match-item's write value, neither of which is "the
494/// per-tick Boolean this edge requires to be taken"). Without a place to put
495/// that leaf reference, nothing in the IR could represent even the simplest
496/// legal sequence, `a` (16.9.1: a lone Boolean expression as a sequence
497/// matches over exactly one tick, true -> match, false -> no match) --
498/// there is no way to say "this edge may be taken on a tick only if `a`
499/// evaluates true" using `Transition` alone. `Edge::guard` closes that gap
500/// at the EDGE level (not a sixth `Transition` variant, preserving T4's own
501/// "every LRM operator is built from these five shapes... not a sixth"
502/// design intent): `guard = Some(leaf)` means this edge is eligible only on
503/// a tick where `leaf` evaluates true; `guard = None` means the edge's own
504/// `label` is the only eligibility condition (a pure elapsed-tick window for
505/// `Delay`, or unconditional for `Epsilon`/`Match`).
506///
507/// Two combinations matter in practice (both built by
508/// `vj-codegen::sva::build`, T9):
509/// - `guard = Some(leaf)` + `label = Epsilon`: 16.9.1's Boolean-leaf
510///   point-sample -- "this tick, `leaf` must evaluate true", costing zero
511///   ADDITIONAL elapsed ticks beyond the one it is sampled on (the thing
512///   that advances a sequence's clock is a `##`/repetition operator
513///   *between* samples, never a sample's own evaluation -- confirmed
514///   against the well-known identity `a[*3]` spans exactly 2 ticks, i to
515///   i+2, not 3, which only holds if a bare leaf's own cost is zero).
516/// - `guard = None` + `label = Delay { min, max }`: the elapsed-tick window
517///   a `##min:max` cycle delay (16.7) or an inter-repetition link (16.9.2's
518///   implicit `##1` between consecutive repeats, or a wider search window
519///   for `[->]`/`[=]`'s nonconsecutive/goto repeats) imposes between two
520///   samples.
521///
522/// A guard has no polarity of its own: the gap of a goto/nonconsecutive
523/// repetition (16.9.2, `b[->n]` == `(!b[*0:$] ##1 b)[*n]`) is guarded by a
524/// SEPARATE leaf standing for the expression `!b`, which the builder marks
525/// negated in its leaf table (`LeafTable::is_negated`). The negation must live
526/// in the leaf, not on the edge, because it is a 4-state operator: `!x` is `x`,
527/// which a Boolean context collapses to false, so `!b` holds only where `b` is
528/// KNOWN false -- not wherever `b` fails to hold, which an edge flag over a
529/// leaf already collapsed to a bool could not tell apart.
530#[derive(Clone, Debug, PartialEq, Eq)]
531pub struct Edge {
532    pub guard: Option<LeafId>,
533    pub label: Transition,
534    pub target: ThreadStateId,
535}
536
537/// One state's outgoing edges -- a row of the automaton's static transition
538/// table (plan §1.2). A state with at least one `Transition::Match` edge
539/// among its `edges` is an accepting state (per [`Transition::Match`]'s own
540/// doc comment: reaching one schedules a pass/fail/cover report); this is a
541/// derived property (see [`State::is_accepting`]), not a separate flag, so
542/// it can never drift out of sync with the edges actually present.
543#[derive(Clone, Debug, Default, PartialEq, Eq)]
544pub struct State {
545    pub edges: Vec<Edge>,
546}
547
548impl State {
549    /// `true` if any of this state's edges is a [`Transition::Match`] --
550    /// see the struct doc comment.
551    pub fn is_accepting(&self) -> bool {
552        self.edges
553            .iter()
554            .any(|e| matches!(e.label, Transition::Match { .. }))
555    }
556}
557
558/// A compiled automaton (plan §1.2's "a dedicated, interpreted automaton per
559/// assertion, compiled once at elaboration time into a static transition
560/// table") -- one sequence/property tree's whole static shape, addressed by
561/// [`ThreadStateId`] (an index into `states`), plus which state a freshly
562/// spawned thread starts in. Pure data, no `sv-lang` dependency (this
563/// module's own doc comment), so it is equally usable by
564/// `vj-codegen::sva::build` (which constructs one from an `AssertionExpr`
565/// tree, under the `frontend` feature) and by the eventual
566/// `vj-runtime::sva_engine` (which only ever interprets an already-built
567/// one, with `vj-sim`'s `frontend` feature off, per this crate's own
568/// `default-features = false` contract with `vj-runtime`).
569#[derive(Clone, Debug, PartialEq, Eq)]
570pub struct Automaton {
571    pub states: Vec<State>,
572    pub start: ThreadStateId,
573    /// `true` when the sequence this automaton was built from admits the
574    /// EMPTY match (16.9.2.1: a match spanning zero clock ticks, e.g.
575    /// `a[*0]`, `a[*0:2]`). The empty match is not a path through `states`
576    /// (a path always consumes at least the tick it starts on), so it cannot
577    /// be an accepting state; this flag carries it separately. A sequence
578    /// that admits the empty match and nothing else can never match when used
579    /// as a property (16.9.2.1 "a sequence that admits only an empty match
580    /// cannot be used as a property"); one that admits both matches on its
581    /// non-empty paths only -- consumers decide how to treat the empty match
582    /// (T10 only records it; the property layers that care are T19+).
583    pub admits_empty: bool,
584    /// How many [`FirstMatchId`]s this automaton uses (`0..first_match_groups`):
585    /// the size of the per-attempt first-match table the engine allocates
586    /// (keyed by group and begin tick, see [`Transition::FirstMatchBegin`]).
587    pub first_match_groups: u32,
588    /// The local-variable slots (`LocalVarId(i)` is `local_vars[i]`): the size
589    /// and element types of the per-thread frame the engine allocates, see
590    /// [`LocalVarId`]. Every slot of every instance expansion has its own entry
591    /// (a slot is never reused for a different variable), so the frame is as
592    /// wide as the assertion's total number of local-variable declarations plus
593    /// the and/intersect shadows.
594    pub local_vars: Vec<LocalVarDecl>,
595    /// The sequence instances whose end points this automaton tests
596    /// (`EndPointId(i)` is `end_points[i]`), each a whole automaton of its own.
597    pub end_points: Vec<EndPoint>,
598    /// The clock a thread is in at [`Automaton::start`] (sva T28): the clock of the
599    /// sequence's first Boolean operand. [`ClockId`]`(0)` unless the sequence opens
600    /// with a clocking event of its own that is not the assertion's.
601    pub entry_clock: ClockId,
602}
603
604impl Automaton {
605    /// An automaton with one (unreachable until wired) state and no edges --
606    /// the empty table a builder starts folding states/edges into.
607    pub fn empty() -> Self {
608        let mut a = Automaton {
609            states: Vec::new(),
610            start: ThreadStateId(0),
611            admits_empty: false,
612            first_match_groups: 0,
613            local_vars: Vec::new(),
614            end_points: Vec::new(),
615            entry_clock: ClockId(0),
616        };
617        let s = a.new_state();
618        a.start = s;
619        a
620    }
621
622    /// Mints the next local variable, of the given integral type, and returns its
623    /// (head) slot. A variable wider than 64 bits gets [`LocalVarDecl::slots`]
624    /// consecutive slots: the head carries the declared type and value limb 0, the
625    /// rest are plain 64-bit unsigned limb slots (value limbs 1.., then the
626    /// unknown limbs), so everything that names a variable by its head slot
627    /// (reads, copies, steps) reaches the others by position.
628    pub fn new_local_var(&mut self, width: u32, signed: bool) -> LocalVarId {
629        let id = LocalVarId(self.local_vars.len() as u32);
630        let decl = LocalVarDecl { width, signed };
631        self.local_vars.push(decl);
632        for _ in 1..decl.slots() {
633            self.local_vars.push(LocalVarDecl {
634                width: 64,
635                signed: false,
636            });
637        }
638        id
639    }
640
641    /// Borrows `id`'s declared type. `id` must be a slot this automaton minted
642    /// (see [`Automaton::add_edge`]'s identical note).
643    pub fn local_var(&self, id: LocalVarId) -> &LocalVarDecl {
644        &self.local_vars[id.0 as usize]
645    }
646
647    /// Registers a sequence instance whose end point this automaton tests.
648    pub fn new_end_point(&mut self, point: EndPoint) -> EndPointId {
649        let id = EndPointId(self.end_points.len() as u32);
650        self.end_points.push(point);
651        id
652    }
653
654    /// Borrows `id`'s instance (an id this automaton minted, like
655    /// [`Automaton::local_var`]).
656    pub fn end_point(&self, id: EndPointId) -> &EndPoint {
657        &self.end_points[id.0 as usize]
658    }
659
660    /// Mints the next [`FirstMatchId`] (one per `first_match` region).
661    pub fn new_first_match_group(&mut self) -> FirstMatchId {
662        let id = FirstMatchId(self.first_match_groups);
663        self.first_match_groups += 1;
664        id
665    }
666
667    /// Allocates a fresh state with no outgoing edges and returns its id.
668    pub fn new_state(&mut self) -> ThreadStateId {
669        let id = ThreadStateId(self.states.len() as u32);
670        self.states.push(State::default());
671        id
672    }
673
674    /// Appends one edge to `from`'s row. `from` must be a state this same
675    /// `Automaton` has already allocated (via [`Automaton::new_state`]) --
676    /// out-of-range ids panic (a builder-internal invariant, never reachable
677    /// from untrusted input: every `ThreadStateId` this crate's builder ever
678    /// hands to this method came from this same `Automaton`'s own
679    /// `new_state`, per the one caller that exists today,
680    /// `vj-codegen::sva::build`).
681    pub fn add_edge(
682        &mut self,
683        from: ThreadStateId,
684        guard: Option<LeafId>,
685        label: Transition,
686        target: ThreadStateId,
687    ) {
688        self.states[from.0 as usize].edges.push(Edge {
689            guard,
690            label,
691            target,
692        });
693    }
694
695    /// Borrows `id`'s row.
696    pub fn state(&self, id: ThreadStateId) -> &State {
697        &self.states[id.0 as usize]
698    }
699
700    /// An upper bound on how many clock ticks past its start tick any match of
701    /// this automaton can end: the sum of every delay edge's upper bound (a simple
702    /// path takes each edge at most once, and a repetition is already unrolled into
703    /// delay edges), or `None` when some delay is unbounded (`##[1:$]`, `[*]`) or a
704    /// [`Transition::ClockSync`] / end-point edge makes the span a function of
705    /// another clock. A cover-sequence attempt (16.14.3) that has run this long has
706    /// reported every match it ever will.
707    pub fn max_span(&self) -> Option<u32> {
708        // A loop through a delay edge (the unbounded repetition `s[*lo:$]` is a
709        // loop of delay-1 edges) takes that edge any number of times: the sum
710        // below would count it once and retire the attempt while it can still
711        // match.
712        for (u, state) in self.states.iter().enumerate() {
713            for edge in &state.edges {
714                let Transition::Delay { min, max } = &edge.label else {
715                    continue;
716                };
717                if (*max).unwrap_or(u32::MAX).max(*min) == 0 {
718                    continue;
719                }
720                let mut seen = vec![false; self.states.len()];
721                let mut stack = vec![edge.target.0 as usize];
722                while let Some(s) = stack.pop() {
723                    if s == u {
724                        return None;
725                    }
726                    if std::mem::replace(&mut seen[s], true) {
727                        continue;
728                    }
729                    stack.extend(self.states[s].edges.iter().map(|e| e.target.0 as usize));
730                }
731            }
732        }
733        let mut total: u32 = 0;
734        for edge in self.states.iter().flat_map(|s| &s.edges) {
735            match &edge.label {
736                Transition::Delay { min, max } => {
737                    total = total.checked_add((*max)?.max(*min))?;
738                }
739                Transition::ClockSync { .. } => return None,
740                // The instance runs free on its own; the testing thread only
741                // reads its status, in no time (see [`Automaton::end_point_lookback`]).
742                Transition::EndPoint { .. }
743                | Transition::Epsilon
744                | Transition::Abort { .. }
745                | Transition::Match { .. }
746                | Transition::LocalVarWrite { .. }
747                | Transition::LocalVarCopy { .. }
748                | Transition::FirstMatchBegin { .. }
749                | Transition::FirstMatchEnd { .. }
750                | Transition::LocalVarStep { .. }
751                | Transition::SubroutineCall { .. } => {}
752            }
753        }
754        Some(total)
755    }
756
757    /// For every sequence instance this automaton (or an instance of it) tests:
758    /// its clock, and how many of that clock's ticks before a testing thread's
759    /// position an attempt of it may have started and still end there (`None`
760    /// for an instance with no bounded span, an unbounded repetition: its status
761    /// needs every past start).
762    pub fn end_point_lookback(&self) -> Vec<(ClockId, Option<u32>)> {
763        let mut out = Vec::new();
764        for ep in &self.end_points {
765            let inner = ep.automaton.end_point_lookback();
766            let deepest = inner.iter().try_fold(0u32, |m, (_, n)| n.map(|n| m.max(n)));
767            let span = ep
768                .automaton
769                .max_span()
770                .zip(deepest)
771                .and_then(|(own, d)| own.checked_add(d));
772            out.push((ep.automaton.entry_clock, span));
773            out.extend(inner);
774        }
775        out
776    }
777
778    /// Whether any edge of this automaton (not of its end-point instances, see
779    /// [`Automaton::end_points_have_calls`]) attaches a subroutine call (16.11).
780    pub fn has_calls(&self) -> bool {
781        self.states
782            .iter()
783            .flat_map(|s| &s.edges)
784            .any(|e| matches!(e.label, Transition::SubroutineCall { .. }))
785    }
786
787    /// Whether a sequence instance an end-point method tests (at any depth)
788    /// attaches a subroutine call.
789    pub fn end_points_have_calls(&self) -> bool {
790        self.end_points
791            .iter()
792            .any(|p| p.automaton.has_calls() || p.automaton.end_points_have_calls())
793    }
794}
795
796#[cfg(test)]
797mod tests {
798    use super::*;
799
800    #[test]
801    fn transition_kind_name_covers_every_variant() {
802        assert_eq!(Transition::Delay { min: 0, max: None }.kind_name(), "Delay");
803        assert_eq!(Transition::Epsilon.kind_name(), "Epsilon");
804        assert_eq!(
805            Transition::Abort {
806                leaf: LeafId(0),
807                mode: AbortMode::Accept
808            }
809            .kind_name(),
810            "Abort"
811        );
812        assert_eq!(Transition::Match { actions: vec![] }.kind_name(), "Match");
813        assert_eq!(
814            Transition::LocalVarWrite {
815                var: LocalVarId(0),
816                value: LeafId(0)
817            }
818            .kind_name(),
819            "LocalVarWrite"
820        );
821        assert_eq!(
822            Transition::FirstMatchBegin {
823                group: FirstMatchId(0)
824            }
825            .kind_name(),
826            "FirstMatchBegin"
827        );
828        assert_eq!(
829            Transition::FirstMatchEnd {
830                group: FirstMatchId(0)
831            }
832            .kind_name(),
833            "FirstMatchEnd"
834        );
835        assert_eq!(
836            Transition::ClockSync {
837                to: ClockId(1),
838                strict: true
839            }
840            .kind_name(),
841            "ClockSync"
842        );
843    }
844
845    /// T28: clocks are small ordered handles, and a fresh automaton starts in the
846    /// assertion's own clock.
847    #[test]
848    fn clock_ids_order_and_default_to_the_assertion_clock() {
849        assert!(ClockId(0) < ClockId(1));
850        assert_eq!(Automaton::empty().entry_clock, ClockId(0));
851    }
852
853    /// T14: the two match-item edges have their own names, and a call's
854    /// arguments are by-value leaves or by-reference markers.
855    #[test]
856    fn match_item_edges_have_their_own_kind_names() {
857        assert_eq!(
858            Transition::LocalVarStep {
859                var: LocalVarId(0),
860                delta: 1
861            }
862            .kind_name(),
863            "LocalVarStep"
864        );
865        let call = Transition::SubroutineCall {
866            call: CallId(3),
867            args: vec![CallArg::Value(LeafId(1)), CallArg::Ref, CallArg::Const],
868        };
869        assert_eq!(call.kind_name(), "SubroutineCall");
870        assert_ne!(CallArg::Value(LeafId(0)), CallArg::Ref);
871        assert_ne!(CallArg::Const, CallArg::Ref);
872        assert!(CallId(1) < CallId(2));
873    }
874
875    /// 16.11: `has_calls` sees a call edge of the automaton itself and, apart,
876    /// one hidden in a sequence instance an end-point method tests.
877    #[test]
878    fn call_edges_are_found_on_the_automaton_and_in_its_end_points() {
879        let call = || Transition::SubroutineCall {
880            call: CallId(0),
881            args: vec![],
882        };
883        let mut plain = Automaton::empty();
884        let s1 = plain.new_state();
885        plain.add_edge(plain.start, None, Transition::Epsilon, s1);
886        assert!(!plain.has_calls() && !plain.end_points_have_calls());
887
888        let mut direct = plain.clone();
889        direct.add_edge(s1, None, call(), s1);
890        assert!(direct.has_calls() && !direct.end_points_have_calls());
891
892        let mut inner = plain.clone();
893        let s2 = inner.new_state();
894        inner.add_edge(s1, None, call(), s2);
895        let mut outer = plain.clone();
896        outer.new_end_point(EndPoint {
897            method: EndMethod::Triggered,
898            site: 0,
899            automaton: inner,
900            flow_out: Vec::new(),
901        });
902        assert!(!outer.has_calls());
903        assert!(outer.end_points_have_calls());
904    }
905
906    /// T15: end points are minted densely from 0, each carrying its own
907    /// automaton, and the edge that tests one has its own kind name.
908    #[test]
909    fn end_points_are_minted_densely_and_have_a_kind_name() {
910        let mut a = Automaton::empty();
911        assert!(a.end_points.is_empty());
912        let sub = Automaton::empty();
913        let p0 = a.new_end_point(EndPoint {
914            method: EndMethod::Triggered,
915            site: 0,
916            automaton: sub.clone(),
917            flow_out: Vec::new(),
918        });
919        let p1 = a.new_end_point(EndPoint {
920            method: EndMethod::Matched,
921            site: 1,
922            automaton: sub,
923            flow_out: vec![(LocalVarId(0), LocalVarId(1))],
924        });
925        assert_eq!((p0, p1), (EndPointId(0), EndPointId(1)));
926        assert_eq!(a.end_point(p0).method, EndMethod::Triggered);
927        assert_eq!(a.end_point(p1).method, EndMethod::Matched);
928        assert_eq!(a.end_point(p1).flow_out, [(LocalVarId(0), LocalVarId(1))]);
929        assert!(p0 < p1);
930        let edge = Transition::EndPoint {
931            point: p0,
932            flag: Some(LocalVarId(2)),
933        };
934        assert_eq!(edge.kind_name(), "EndPoint");
935        assert_ne!(
936            edge,
937            Transition::EndPoint {
938                point: p0,
939                flag: None
940            }
941        );
942    }
943
944    #[test]
945    fn local_var_copy_has_its_own_kind_name() {
946        assert_eq!(
947            Transition::LocalVarCopy {
948                dst: LocalVarId(1),
949                src: LocalVarId(0)
950            }
951            .kind_name(),
952            "LocalVarCopy"
953        );
954    }
955
956    /// T13: local-variable slots are minted densely from 0 and carry the
957    /// declared type the engine casts every write to (16.10: the value of a
958    /// local variable is always a value of its declared type).
959    #[test]
960    fn local_vars_are_minted_densely_with_their_declared_type() {
961        let mut a = Automaton::empty();
962        assert!(a.local_vars.is_empty());
963        let x = a.new_local_var(32, true);
964        let y = a.new_local_var(4, false);
965        assert_eq!((x, y), (LocalVarId(0), LocalVarId(1)));
966        assert_eq!(
967            a.local_var(x),
968            &LocalVarDecl {
969                width: 32,
970                signed: true
971            }
972        );
973        assert_eq!(
974            a.local_var(y),
975            &LocalVarDecl {
976                width: 4,
977                signed: false
978            }
979        );
980        assert!(LocalVarId(0) < LocalVarId(1));
981    }
982
983    /// T13: [`LocalVarDecl::cast`] is the one place a value is fitted to a
984    /// local variable's type: truncate to the width, then sign-extend for a
985    /// signed type -- exactly SystemVerilog's integral cast.
986    #[test]
987    fn local_var_decl_cast_truncates_then_extends() {
988        let u4 = LocalVarDecl {
989            width: 4,
990            signed: false,
991        };
992        assert_eq!(u4.cast(0x1f), 0xf, "unsigned keeps the low bits");
993        assert_eq!(u4.cast(-1), 0xf, "a negative value wraps");
994        let s4 = LocalVarDecl {
995            width: 4,
996            signed: true,
997        };
998        assert_eq!(s4.cast(0xf), -1, "bit 3 set sign-extends");
999        assert_eq!(s4.cast(0x7), 7);
1000        assert_eq!(s4.cast(0x18), -8, "0x18 truncates to 0x8, which is -8");
1001        let i32d = LocalVarDecl {
1002            width: 32,
1003            signed: true,
1004        };
1005        assert_eq!(i32d.cast(1 << 31), i64::from(i32::MIN));
1006        assert_eq!(i32d.cast(-5), -5);
1007        let u1 = LocalVarDecl {
1008            width: 1,
1009            signed: false,
1010        };
1011        assert_eq!((u1.cast(2), u1.cast(3)), (0, 1));
1012        // 64 bits and wider (the interpreter's value is an i64) are unchanged.
1013        let w64 = LocalVarDecl {
1014            width: 64,
1015            signed: false,
1016        };
1017        assert_eq!(w64.cast(-1), -1);
1018        let w128 = LocalVarDecl {
1019            width: 128,
1020            signed: true,
1021        };
1022        assert_eq!(w128.cast(i64::MIN), i64::MIN);
1023    }
1024
1025    /// T11: first-match groups are minted densely from 0, and the count is
1026    /// what the engine sizes its per-attempt table by.
1027    #[test]
1028    fn first_match_groups_are_minted_densely() {
1029        let mut a = Automaton::empty();
1030        assert_eq!(a.first_match_groups, 0);
1031        assert_eq!(a.new_first_match_group(), FirstMatchId(0));
1032        assert_eq!(a.new_first_match_group(), FirstMatchId(1));
1033        assert_eq!(a.first_match_groups, 2);
1034        assert!(FirstMatchId(0) < FirstMatchId(1));
1035    }
1036
1037    #[test]
1038    fn abort_mode_arity_and_16_12_14_cross_product() {
1039        assert_eq!(AbortMode::ALL.len(), AbortMode::COUNT);
1040        // accept_on: async, accepts.
1041        assert!(!AbortMode::Accept.is_sync());
1042        assert!(AbortMode::Accept.accepts());
1043        // reject_on: async, rejects.
1044        assert!(!AbortMode::Reject.is_sync());
1045        assert!(!AbortMode::Reject.accepts());
1046        // sync_accept_on: sync, accepts.
1047        assert!(AbortMode::SyncAccept.is_sync());
1048        assert!(AbortMode::SyncAccept.accepts());
1049        // sync_reject_on: sync, rejects.
1050        assert!(AbortMode::SyncReject.is_sync());
1051        assert!(!AbortMode::SyncReject.accepts());
1052    }
1053
1054    #[test]
1055    fn ids_are_distinct_and_hashable() {
1056        use std::collections::HashSet;
1057        let mut set = HashSet::new();
1058        for i in 0..4u32 {
1059            assert!(set.insert(AutomatonId(i)));
1060        }
1061        assert_ne!(ThreadStateId(0), ThreadStateId(1));
1062        assert_ne!(LeafId(0), LeafId(1));
1063        assert_ne!(ActionId(0), ActionId(1));
1064        assert_ne!(LocalVarId(0), LocalVarId(1));
1065    }
1066
1067    /// T9: [`Automaton::empty`]/`new_state`/`add_edge` wire up a real graph,
1068    /// and [`State::is_accepting`] tracks exactly the states with a `Match`
1069    /// edge, no more and no less.
1070    #[test]
1071    fn automaton_wiring_and_is_accepting() {
1072        let mut a = Automaton::empty();
1073        assert!(
1074            !a.admits_empty,
1075            "a fresh automaton admits no empty match until a builder says so"
1076        );
1077        let start = a.start;
1078        let mid = a.new_state();
1079        let accept = a.new_state();
1080        a.add_edge(start, Some(LeafId(0)), Transition::Epsilon, mid);
1081        assert!(
1082            !a.state(start).is_accepting(),
1083            "start has only a guarded Epsilon edge, no Match edge yet"
1084        );
1085        a.add_edge(mid, None, Transition::Match { actions: vec![] }, accept);
1086        assert!(
1087            a.state(mid).is_accepting(),
1088            "mid now has a Match edge among its others"
1089        );
1090        assert!(
1091            !a.state(accept).is_accepting(),
1092            "accept itself has no outgoing edges at all yet"
1093        );
1094        assert_eq!(a.state(start).edges.len(), 1);
1095        assert_eq!(a.state(start).edges[0].guard, Some(LeafId(0)));
1096        assert_eq!(a.state(start).edges[0].target, mid);
1097    }
1098
1099    /// T29: the span bound sums the delay edges of a straight-line automaton,
1100    /// but a loop through a delay edge (`s[*lo:$]`) has no bound: counting its
1101    /// edge once retired a cover-sequence attempt after three matches of
1102    /// `x ##2 y[*3:$]` while it could still match.
1103    #[test]
1104    fn max_span_is_unbounded_through_a_delay_loop() {
1105        let mut a = Automaton::empty();
1106        let (s0, s1) = (a.start, a.new_state());
1107        a.add_edge(
1108            s0,
1109            None,
1110            Transition::Delay {
1111                min: 2,
1112                max: Some(2),
1113            },
1114            s1,
1115        );
1116        assert_eq!(a.max_span(), Some(2));
1117        let s2 = a.new_state();
1118        a.add_edge(
1119            s1,
1120            None,
1121            Transition::Delay {
1122                min: 1,
1123                max: Some(1),
1124            },
1125            s2,
1126        );
1127        a.add_edge(s2, None, Transition::Epsilon, s1);
1128        assert_eq!(
1129            a.max_span(),
1130            None,
1131            "a delay inside a loop repeats unboundedly"
1132        );
1133        // A loop of only epsilon edges takes no time: the bound stays finite.
1134        let mut b = Automaton::empty();
1135        let (t0, t1) = (b.start, b.new_state());
1136        b.add_edge(
1137            t0,
1138            None,
1139            Transition::Delay {
1140                min: 1,
1141                max: Some(1),
1142            },
1143            t1,
1144        );
1145        b.add_edge(t1, None, Transition::Epsilon, t1);
1146        assert_eq!(b.max_span(), Some(1));
1147    }
1148}