sv_sim_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 (`sv-sim-sim::sva::Automaton`), stepped by a small engine in
5//! `sv-sim-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 `sv-sim-codegen::sva::{ranges, op}`'s
8//! already-recovered delay/repetition bounds and operator values) and no
9//! engine (`sv-sim-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` (`sv-sim-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 `sv-sim-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 /// `sv-sim-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/// `sv-sim-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/// `sv-sim-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/// `sv-sim-codegen::sva::build` (which constructs one from an `AssertionExpr`
565/// tree, under the `frontend` feature) and by the eventual
566/// `sv-sim-runtime::sva_engine` (which only ever interprets an already-built
567/// one, with `sv-sim-sim`'s `frontend` feature off, per this crate's own
568/// `default-features = false` contract with `sv-sim-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 /// `sv-sim-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}