Skip to main content

sv_sim_sim/netlist/
specify.rs

1//! The per-design, per-instance, runtime-mutable timing table every
2//! SDF-annotatable value lives in (`docs/superpowers/plans/2026-10-03-sdf.md`
3//! §1.1/§1.3, Task 2). Path delays, pulse limits, timing-check limits and
4//! specparam values are never baked as Rust closures' captured constants or
5//! LLVM constants — they are entries in [`TimingModel`], read fresh by the
6//! delay-line/timing-check processes (Track A/B, `sv-sim-bin::specify`/
7//! `sv-sim-bin::timingcheck`) on every firing, so a later `$sdf_annotate`
8//! call (Track C) can mutate them in place mid-run with zero interaction
9//! with codegen.
10//!
11//! Like every other `sv_lang`-touching file in this module (`dedup.rs`,
12//! `elaborate.rs`, `instance.rs`, `items.rs`...), this one lives behind the
13//! `frontend` feature: its key type, [`crate::symid::SymbolId`], is itself
14//! `frontend`-gated, so there is no narrower cfg split available for this
15//! file alone. "No `sv_lang` dependency on the mutation path"
16//! (Task 2's own acceptance text) is therefore a *data*-dependency claim,
17//! not a crate-dependency one: [`TimingModel::set_specparam`] — what a
18//! later `$sdf_annotate` `LABEL` annotation (Task 28) calls from
19//! `sv-sim-runtime` through this crate's `frontend`-enabled build — takes
20//! no `EvalSession`/live `Design`/`Symbol`, only the plain, fully-owned
21//! `sv_lang::ConstantValue` (which, unlike most `sv_lang` types, carries no
22//! `'d` lifetime at all — see its own doc comment) and this module's own
23//! key types. Only [`TimingModel::seed_specparams`] needs a live `Design`'s
24//! `Symbol`s at all.
25//!
26//! Task 2 is a module skeleton only: [`PathEntry`]/[`CheckEntry`] are bare
27//! shells (just enough shape for [`TimingModel`]'s two `Vec`s to exist) —
28//! Track A (`T3`-`T11`) and Track B (`T13`+) fill in the rest of §1.1's own
29//! field list as each construct's discovery/planning lands.
30
31use std::collections::HashMap;
32
33use sv_lang::kinds::{ExpressionKind, SyntaxKind};
34use sv_lang::nodes::{
35    ConditionalPathDeclarationSyntax, IfNonePathDeclarationSyntax, PathDeclarationSyntax,
36    PathSuffixSyntax,
37};
38use sv_lang::{
39    ArgumentDirection, BinaryOp, Bit, ConstantValue, EvalSession, Expression, PulseStyleKind,
40    Symbol, SyntaxTree, TimingPathPolarity, UnaryOp,
41};
42use sv_lang_kinds::SymbolKind;
43use sv_sim_abi::bit4::Bit4;
44
45use super::{ElabError, InstanceId};
46use crate::symid::{SymbolId, SymbolUid, is_value_name};
47
48pub use crate::timing_types::{
49    CondBinaryOp, CondUnaryOp, EdgeControl, EdgeSet, EdgeTransition, PathDelaySet, PulseStyle,
50    TRANSITIONS,
51};
52
53/// A module-path terminal, resolved to its signal and (for a bit-select or
54/// part-select terminal) the bit window within it: `(lo, width)`, `lo` the
55/// least significant bit index and `width` the number of bits. `None` for a
56/// terminal that names the whole signal.
57pub type TerminalRef = (SymbolId, Option<(u32, u32)>);
58
59/// A module path's own edge-sensitivity (LRM 1800-2023 §30.4.3, Task 7 —
60/// `docs/superpowers/plans/2026-10-03-sdf.md`).
61///
62/// Derived from TWO sv-lang accessors together, never one alone:
63/// `Symbol::timing_path_edge_identifier()` alone cannot distinguish "this is
64/// a plain, non-edge-sensitive path" from "this is an edge-sensitive path
65/// with no edge keyword written" — BOTH report [`sv_lang::EdgeKind::None`]
66/// (verified live against sv-lang=0.1.9/slang 11.0: `(posedge clk => q) =
67/// 1;` — the edge-sensitive grammar's OWN "simple destination" alternative,
68/// no `(dest:data)` suffix at all — reports `PosEdge`, proving the
69/// identifier is a direct field of the edge-sensitive grammar's shared
70/// `[edge_identifier]` production regardless of which destination suffix
71/// alternative is used; `(clock => (out:in))=10;` — LRM 30.4.3's own
72/// Example 3, no edge keyword at all but WITH the `(dest:data)` suffix —
73/// reports `None` for the identifier yet `Some` for
74/// `timing_path_edge_source_expr()`; a PLAIN 30.4.2 simple path reports
75/// `None`/`None` for both). So the discriminator for "is this path
76/// edge-sensitive AT ALL" is `identifier != EdgeKind::None ||
77/// edge_source_expr().is_some()`, and [`PathEdge::Any`] is exactly the
78/// "edge-sensitive, but no keyword written" case LRM 30.4.3's own text
79/// gives an explicit rule for: "If the edge transition is not specified,
80/// the path shall be considered active on any transition at the input
81/// terminal."
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub enum PathEdge {
84    /// Not an edge-sensitive path at all (LRM 30.4.2 simple paths, 30.4.5
85    /// full/parallel connection paths with no edge-sensitive suffix) — no
86    /// edge gating whatsoever; `sv-sim-bin::specify::PathRetimer` fires on
87    /// any transition of the destination's own staging cell, exactly the
88    /// pre-Task-7 behavior every earlier task's fixture already exercises.
89    None,
90    /// An edge-sensitive path (30.4.3) with no `edge_identifier` written —
91    /// gated on "any transition at the input terminal" (30.4.3's own text,
92    /// quoted above), i.e. `from != to` on the member's own input, with no
93    /// further direction requirement.
94    Any,
95    /// An edge-sensitive path with a specific `posedge`/`negedge`/`edge`
96    /// identifier — never [`sv_lang::EdgeKind::None`] here (that raw value
97    /// is exactly what distinguishes [`PathEdge::None`]/[`PathEdge::Any`]
98    /// from this variant in the first place, so constructing `Specific`
99    /// with `EdgeKind::None` would be a caller bug, not a real state; see
100    /// [`PathEdge::from_accessors`]'s own body, the only constructor).
101    Specific(sv_lang::EdgeKind),
102}
103
104impl PathEdge {
105    /// The one, single call site for `Symbol::timing_path_edge_source_expr()`
106    /// across this whole crate — returns the derived [`PathEdge`] alongside
107    /// the edge-sensitive suffix's own data-source expression (LRM 30.4.3:
108    /// "an arbitrary expression, which serves as a description of the flow
109    /// of data to the path destination. This arbitrary data path
110    /// description does not affect the actual propagation of data or
111    /// events"), so a caller that also wants to store that expression for
112    /// debug/VPI purposes (never for simulation effect — Task 7's own
113    /// acceptance text) never needs a second call of its own.
114    fn from_accessors<'d>(path: &Symbol<'d>) -> (PathEdge, Option<Expression<'d>>) {
115        let identifier = path.timing_path_edge_identifier();
116        let source = path.timing_path_edge_source_expr();
117        let edge = if identifier != sv_lang::EdgeKind::None {
118            PathEdge::Specific(identifier)
119        } else if source.is_some() {
120            PathEdge::Any
121        } else {
122            PathEdge::None
123        };
124        (edge, source)
125    }
126}
127
128impl CondUnaryOp {
129    /// `None` for any of `UnaryOp`'s other six variants (`Plus`/`Minus`/the
130    /// four pre/post-increment/decrement forms) -- none of which Table
131    /// 30-1 lists, so none is a legal state-dependent-path condition
132    /// operator.
133    fn from_sv(op: UnaryOp) -> Option<CondUnaryOp> {
134        match op {
135            UnaryOp::BitwiseNot => Some(CondUnaryOp::BitwiseNot),
136            UnaryOp::BitwiseAnd => Some(CondUnaryOp::ReductionAnd),
137            UnaryOp::BitwiseOr => Some(CondUnaryOp::ReductionOr),
138            UnaryOp::BitwiseXor => Some(CondUnaryOp::ReductionXor),
139            UnaryOp::BitwiseNand => Some(CondUnaryOp::ReductionNand),
140            UnaryOp::BitwiseNor => Some(CondUnaryOp::ReductionNor),
141            UnaryOp::BitwiseXnor => Some(CondUnaryOp::ReductionXnor),
142            UnaryOp::LogicalNot => Some(CondUnaryOp::LogicalNot),
143            UnaryOp::Plus
144            | UnaryOp::Minus
145            | UnaryOp::Preincrement
146            | UnaryOp::Predecrement
147            | UnaryOp::Postincrement
148            | UnaryOp::Postdecrement => None,
149        }
150    }
151}
152
153impl CondBinaryOp {
154    /// The operator set of a timing check's `&&&`/timestamp/timecheck
155    /// condition (LRM 31.7, `scalar_timing_check_condition`: `==`, `===`,
156    /// `!=`, `!==`, and the Table 30-1 operators slang's own parse allows):
157    /// [`Self::from_sv`]'s set plus the two case-equality operators.
158    fn from_sv_check(op: BinaryOp) -> Option<CondBinaryOp> {
159        match op {
160            BinaryOp::CaseEquality => Some(CondBinaryOp::CaseEquality),
161            BinaryOp::CaseInequality => Some(CondBinaryOp::CaseInequality),
162            other => CondBinaryOp::from_sv(other),
163        }
164    }
165
166    /// `None` for every other `BinaryOp` variant (arithmetic, relational,
167    /// shift, power, case/wildcard (in)equality, implication/equivalence)
168    /// -- Table 30-1 lists none of them.
169    fn from_sv(op: BinaryOp) -> Option<CondBinaryOp> {
170        match op {
171            BinaryOp::BinaryAnd => Some(CondBinaryOp::BitwiseAnd),
172            BinaryOp::BinaryOr => Some(CondBinaryOp::BitwiseOr),
173            BinaryOp::BinaryXor => Some(CondBinaryOp::BitwiseXor),
174            BinaryOp::BinaryXnor => Some(CondBinaryOp::BitwiseXnor),
175            BinaryOp::Equality => Some(CondBinaryOp::Equality),
176            BinaryOp::Inequality => Some(CondBinaryOp::Inequality),
177            BinaryOp::LogicalAnd => Some(CondBinaryOp::LogicalAnd),
178            BinaryOp::LogicalOr => Some(CondBinaryOp::LogicalOr),
179            BinaryOp::Add
180            | BinaryOp::Subtract
181            | BinaryOp::Multiply
182            | BinaryOp::Divide
183            | BinaryOp::Mod
184            | BinaryOp::CaseEquality
185            | BinaryOp::CaseInequality
186            | BinaryOp::GreaterThanEqual
187            | BinaryOp::GreaterThan
188            | BinaryOp::LessThanEqual
189            | BinaryOp::LessThan
190            | BinaryOp::WildcardEquality
191            | BinaryOp::WildcardInequality
192            | BinaryOp::LogicalImplication
193            | BinaryOp::LogicalEquivalence
194            | BinaryOp::LogicalShiftLeft
195            | BinaryOp::LogicalShiftRight
196            | BinaryOp::ArithmeticShiftLeft
197            | BinaryOp::ArithmeticShiftRight
198            | BinaryOp::Power => None,
199        }
200    }
201}
202
203/// A state-dependent path's own `if (cond)` conditional expression (LRM
204/// 30.4.4.1, Task 8), resolved to data that needs no further `sv_lang`/live
205/// `Design` access to evaluate -- the same "discover the SHAPE now,
206/// evaluate the VALUE later, against live simulation state" split
207/// [`PathEntry::delays`]'s own doc comment already establishes, except a
208/// condition's VALUE is never baked even once at plan time (unlike a
209/// delay): LRM 30.4.4.1's own operand list ("module input/inout ports...
210/// locally defined variables or nets... compile-time constants") means a
211/// condition's truth genuinely depends on LIVE simulation state, re-read on
212/// every check (`sv-sim-bin::specify`'s own `CondEval`, this type's
213/// plan-time-`Loc`/register-time-`Place`-resolved sibling, is the runtime
214/// evaluator -- this crate carries only the symbolic shape).
215#[derive(Clone, Debug, PartialEq, Eq)]
216pub enum CondNode {
217    /// A signal terminal (whole signal, or a constant bit-select/
218    /// part-select of one) -- LRM 30.4.4.1's own "ports... variables or
219    /// nets... or their bit-/part-selects".
220    Signal(TerminalRef),
221    /// A `Specparam` symbol -- LRM 30.4.4.1's own "compile-time constants
222    /// (constant numbers and specify parameters)". Resolved against
223    /// `TimingModel::specparam` at `sv-sim-bin::specify::plan` time (not
224    /// evaluated here, and not carried as a live value past that point --
225    /// see that crate's own `compile_cond_plan` doc comment for why a
226    /// condition's specparam operand is, as of this task, baked once at
227    /// plan time rather than re-read on every check the way a `Signal`
228    /// leaf is).
229    Specparam(SymbolId),
230    /// A plain compile-time-constant value (LRM 30.4.4.1's own "constant
231    /// numbers"), already evaluated at discovery time -- LSB-first, in
232    /// [`sv_lang::SVInt::bits`]'s own bit order.
233    Const(Vec<Bit4>),
234    Unary(CondUnaryOp, Box<CondNode>),
235    Binary(CondBinaryOp, Box<CondNode>, Box<CondNode>),
236    /// `c ? t : f` -- the plain (non-pattern-matching) ternary form only;
237    /// a `&&&`-chained pattern-matching conditional is rejected at
238    /// [`compile_cond_expr`] (Table 30-1 does not list it).
239    Ternary(Box<CondNode>, Box<CondNode>, Box<CondNode>),
240    /// `{a, b, ...}`, in SOURCE order (first element MSB-most, matching
241    /// `Expression::operands()`'s own documented order) -- never
242    /// reordered here; `sv-sim-bin::specify`'s own runtime evaluator is
243    /// where MSB-first source order meets this crate's own LSB-first
244    /// per-bit storage convention.
245    Concat(Vec<CondNode>),
246    /// `{n{x}}` -- `n`, the replication count, already evaluated to a
247    /// compile-time constant at discovery time (LRM 11.4.12.1's own
248    /// requirement, verified live: slang already enforces this upstream).
249    Replication(u32, Box<CondNode>),
250}
251
252/// A `TimingPath`'s own state-dependency (LRM 30.4.4, Task 8): which of
253/// 30.4.4's three forms -- unconditional, `if (cond)`, or `ifnone` -- this
254/// path was declared with. Discriminated jointly from `Symbol::
255/// timing_path_is_state_dependent()` and `Symbol::
256/// timing_path_condition_expr()` (see [`resolve_condition`]'s own doc
257/// comment for why neither alone suffices -- this plan's own "What already
258/// exists" section's decision 0).
259#[derive(Clone, Debug, PartialEq, Eq)]
260pub enum PathCondition {
261    /// A plain, unconditional module path (30.4.2/30.4.3/30.4.5) -- the
262    /// pre-Task-8 default every earlier task's own fixture already
263    /// exercises.
264    Unconditional,
265    /// `if (cond) ...` -- `cond`, compiled to a [`CondNode`].
266    If(CondNode),
267    /// `ifnone ...` -- LRM 30.4.4.4: active whenever every OTHER
268    /// state-dependent declaration sharing this path's own exact source
269    /// and destination is false (`sv-sim-bin::specify::plan`'s own
270    /// `ifnone_gate` synthesizes this from its sibling `If` members'
271    /// conditions; a path with no such siblings behaves as
272    /// [`Self::Unconditional`], per that same subclause's own text).
273    IfNone,
274}
275
276/// A `TimingPath` (LRM §30.4), post multi-path desugaring (30.4.6): one
277/// [`PathEntry`] per individual module path assignment, after a multi-input/
278/// multi-output full-connection declaration (`(a, b, c *> q1, q2) = 10;`)
279/// has been expanded into its six (here: `inputs.len() * outputs.len()`)
280/// one-to-one assignments per 30.4.6's own worked example. `inputs`/
281/// `outputs` therefore always have exactly one element each as of this task
282/// (Task 3) — kept as `Vec` (matching the plan's own §1.1 pseudocode)
283/// because a later task's vector-terminal handling reads them the same way
284/// regardless of how many elements they end up holding.
285///
286/// Bare shell beyond this — Track A (Task 4a onward) fills in the rest of
287/// §1.1's own field list (`polarity`/`edge`/`condition`/`delays`/
288/// `reject_limit`/`error_limit`/`pulse_style`/`showcancelled`).
289#[derive(Clone, Debug, PartialEq, Eq)]
290pub struct PathEntry {
291    /// The instance the path's declaration lives in.
292    pub instance: InstanceId,
293    /// This path's one resolved source terminal (LRM 30.4.1: always a net).
294    pub inputs: Vec<TerminalRef>,
295    /// This path's one resolved destination terminal (LRM 30.4.1: a net or
296    /// a variable).
297    pub outputs: Vec<TerminalRef>,
298    /// `*>` (`Full`) or `=>` (`Parallel`) — metadata only at this task; T4a
299    /// is where it starts to matter (full connection's fan-in/fan-out vs.
300    /// parallel's bit-paired semantics).
301    pub connection: sv_lang::TimingPathConnectionKind,
302    /// This path's own overall polarity (LRM §30.4.7: the `+`/`-` on the
303    /// path operator itself, e.g. the `+` of `(a +*> b) = 1;` — `path.
304    /// timing_path_polarity()`, NOT `timing_path_edge_polarity()`, which is
305    /// an edge-sensitive path's own, separate destination-suffix polarity
306    /// and T7's scope, not this one). Per §30.4.7.1/.2/.3's own text — "does
307    /// not affect the actual propagation of data" — this is metadata ONLY:
308    /// stored here so a debug/VPI-facing accessor ([`PathEntry::polarity`])
309    /// can read it back, and never consulted by any delay/timing
310    /// computation. `sv-sim-bin::specify::PathRetimer` — the only runtime
311    /// reader of a [`PathEntry`]'s timing data — never reads this field (its
312    /// own fields are `src`/`delays`/`prev`/`shared`/`kick`, and `plan`
313    /// never copies `polarity` into `RetimerPlan`); see that crate's own
314    /// `path_retimer_never_reads_polarity_in_its_delay_computation` test for
315    /// the behavioral proof.
316    pub polarity: TimingPathPolarity,
317    /// The ORIGINAL (pre-desugaring) `TimingPath` symbol's own identity
318    /// (LRM 30.4.6): several desugared [`PathEntry`]s share one `path_uid`
319    /// when they came from one multi-terminal declaration, since they also
320    /// share its one delay list. `sv-sim-bin`'s own plan-time delay
321    /// evaluation (Task 4a) uses it to re-locate that original symbol --
322    /// see [`PathEntry::delays`]'s own doc comment for why evaluation
323    /// cannot happen inside this crate.
324    pub path_uid: SymbolId,
325    /// This path's own 12-entry transition-delay table (LRM Table 30-2/
326    /// 30-3), in scheduler ticks. [`PathDelaySet::default()`] (all zero)
327    /// until `sv-sim-bin`'s own plan-time pass attaches the real,
328    /// timescale-converted value via [`TimingModel::set_path_delays`] --
329    /// evaluating a `path_delay_value` expression needs both
330    /// `sv_lang::EvalSession` (constant-folding) and `sv-sim-codegen::
331    /// timescale` (unit/precision conversion); this crate's own `frontend`
332    /// feature carries the former but can never depend on the latter
333    /// (`sv-sim-codegen` depends on `sv-sim-sim`, never the reverse), so
334    /// the two-step "discover the SHAPE, then attach the VALUE" split this
335    /// field forces mirrors [`TimingModel::set_specparam`]'s own,
336    /// already-established precedent exactly. Never read before that
337    /// attach call runs (`crate::specify::discover`'s own call order,
338    /// `sv-sim-bin`'s `run.rs` wiring) -- the module-path retimer process
339    /// is the only runtime reader, and it is planned strictly after this
340    /// field is set.
341    pub delays: PathDelaySet,
342    /// This path's own edge-sensitivity (LRM 30.4.3, Task 7) — see
343    /// [`PathEdge`]'s own doc comment. [`sv-sim-bin::specify::PathRetimer`]
344    /// is the only runtime reader, and reads it to GATE which of this
345    /// destination's candidate module paths are "active" for a given
346    /// transition (LRM 30.4.3's own text: "which only occur when a
347    /// specified edge occurs at the source signal"), never to decide the
348    /// destination's VALUE (still never this module's job — decision 3,
349    /// unchanged by this task).
350    pub edge: PathEdge,
351    /// The edge-sensitive suffix's own data-source expression (LRM 30.4.3:
352    /// "the `d` of `(posedge clk => (q +: d))`"), reduced to its referenced
353    /// symbol's own name for debug/VPI purposes — `None` both for a
354    /// non-edge-sensitive path and for an edge-sensitive one whose data
355    /// source is not a plain named reference (an arbitrary expression per
356    /// the LRM's own text; losing fidelity for a complex expression is
357    /// harmless, since this field is documented, by both the LRM ("does not
358    /// affect the actual propagation of data or events") and this crate's
359    /// own [`Self::debug_edge_source_expr_name`] accessor, as read ONCE
360    /// here (via [`PathEdge::from_accessors`], [`Self::edge`]'s own sibling
361    /// value from the SAME call) and never again — see
362    /// `sv-sim-bin::specify`'s own
363    /// `edge_source_expr_text_never_affects_scheduled_timing` test for the
364    /// behavioral proof.
365    pub edge_data_source_name: Option<String>,
366    /// This path's own state-dependency (LRM 30.4.4, Task 8) -- see
367    /// [`PathCondition`]'s own doc comment. `sv-sim-bin::specify::
368    /// PathRetimer::select_delay` reads it (through `sv-sim-bin`'s own
369    /// plan-time `CondPlan`/register-time `CondEval` compilation of the
370    /// `CondNode` it carries) to gate this entry's own eligibility
371    /// alongside [`Self::edge`]'s existing edge gate -- both decision 3's
372    /// "only when a specify path is actually active for it."
373    pub condition: PathCondition,
374    /// This path's own per-transition REJECT limit (LRM 30.7, Task 10), in
375    /// scheduler ticks: an output pulse narrower than the limit of the
376    /// delay that forms its TRAILING edge is cancelled outright. Defaults
377    /// (LRM 30.7: "if no limits are specified, they default to the path
378    /// delay") to [`Self::delays`] itself -- every transition's own delay --
379    /// which is exactly full inertial delay; [`TimingModel::set_path_delays`]
380    /// re-establishes that default every time a path's delays are (re)set,
381    /// and [`TimingModel::set_path_pulse_limits`] (a `PATHPULSE$`) overrides
382    /// it afterwards.
383    pub reject_limit: PathDelaySet,
384    /// This path's own per-transition ERROR limit (LRM 30.7, Task 10): an
385    /// output pulse at least as wide as the reject limit but narrower than
386    /// this one is filtered to `x`; one at least this wide passes. Same
387    /// default and override rules as [`Self::reject_limit`]. Never smaller
388    /// than it (see [`TimingModel::set_path_pulse_limits`]).
389    pub error_limit: PathDelaySet,
390    /// This path's own output's pulse style (LRM 30.7.4.1, Task 11): whether
391    /// the `x` that marks a filtered pulse (or, with [`Self::showcancelled`],
392    /// a negative pulse) starts at the leading edge's scheduled time or at the
393    /// moment the pulse is detected. [`PulseStyle::OnEvent`] unless a
394    /// `pulsestyle_ondetect` declaration names an output window overlapping
395    /// this path's own ([`TimingModel::discover`] applies the declarations).
396    pub pulse_style: PulseStyle,
397    /// Whether this path's own output shows a negative pulse (LRM 30.7.4.2,
398    /// Task 11) as `x` rather than cancelling it: `false` (`noshowcancelled`,
399    /// the LRM's own default) unless a `showcancelled` declaration names an
400    /// overlapping output window. Same application rule as
401    /// [`Self::pulse_style`]; the two settings are independent.
402    pub showcancelled: bool,
403}
404
405impl PathEntry {
406    /// This path's own overall polarity (see [`Self::polarity`]'s own field
407    /// doc) — the debug/VPI-facing accessor the plan's own Task 4a
408    /// acceptance text asks for. Metadata only: never called by
409    /// `sv-sim-bin::specify::PathRetimer`'s delay computation, only by a
410    /// debugger/VPI host wanting to report a path's declared polarity back
411    /// to the user.
412    pub fn debug_polarity(&self) -> TimingPathPolarity {
413        self.polarity
414    }
415
416    /// This path's own edge-sensitive data-source expression, reduced to a
417    /// debug name (see [`Self::edge_data_source_name`]'s own field doc) —
418    /// the Task 7 sibling of [`Self::debug_polarity`]: metadata only, never
419    /// called by `sv-sim-bin::specify::PathRetimer`'s delay computation.
420    pub fn debug_edge_source_expr_name(&self) -> Option<&str> {
421        self.edge_data_source_name.as_deref()
422    }
423}
424
425impl EdgeControl {
426    /// The control of "the reference event signal with opposite edge" (LRM
427    /// 31.4.4, `$width`'s derived data event; `$nochange`'s trailing edge is the
428    /// same derivation): every transition reversed. `None` for
429    /// [`EdgeControl::AnyChange`], which has no direction to oppose -- a check
430    /// that needs this has an edge-specified reference (T13 enforces it), so a
431    /// caller meeting `None` reports it by name rather than guessing.
432    pub fn opposite(self) -> Option<EdgeControl> {
433        match self {
434            EdgeControl::AnyChange => None,
435            EdgeControl::Edges(set) => Some(EdgeControl::Edges(set.reversed())),
436        }
437    }
438
439    /// Resolves the parsed `edge` / `edge_descriptors` pair of a
440    /// `SystemTimingCheckArg` (sv-lang keeps the descriptors as written).
441    ///
442    /// * `posedge`/`negedge` are the lists LRM 31.5 states they abbreviate;
443    /// * `edge [d, ...]` is the union of its descriptors;
444    /// * a bare `edge` (no list) is NOT defined by 31.5's text. It is taken as
445    ///   all six transitions -- "any edge", as 9.4.2's `@(edge e)` -- chosen
446    ///   once here.
447    ///
448    /// A descriptor that is not one of the six, or a descriptor list on
449    /// anything but `edge`, is a named `Err` (slang rejects the former at
450    /// parse time; this is the backstop for an sv-lang that stops doing so).
451    pub fn from_syntax(edge: sv_lang::EdgeKind, descriptors: &[String]) -> Result<Self, String> {
452        use sv_lang::EdgeKind as K;
453        match (edge, descriptors.is_empty()) {
454            (K::None, true) => Ok(EdgeControl::AnyChange),
455            (K::PosEdge, true) => Ok(EdgeControl::Edges(EdgeSet::POSEDGE)),
456            (K::NegEdge, true) => Ok(EdgeControl::Edges(EdgeSet::NEGEDGE)),
457            (K::BothEdges, true) => Ok(EdgeControl::Edges(EdgeSet::ALL)),
458            (K::BothEdges, false) => {
459                let mut set = EdgeSet::EMPTY;
460                for d in descriptors {
461                    let t = EdgeTransition::of_descriptor(d).ok_or_else(|| {
462                        format!(
463                            "edge descriptor `{d}` is not one of 01, 10, 0x, x0, 1x, x1 (z \
464                             allowed for x, LRM 31.5)"
465                        )
466                    })?;
467                    set = set.with(t);
468                }
469                Ok(EdgeControl::Edges(set))
470            }
471            (K::None | K::PosEdge | K::NegEdge, false) => Err(format!(
472                "an edge descriptor list {descriptors:?} is only allowed on `edge [...]`, not \
473                 on {edge:?} (LRM 31.5)"
474            )),
475        }
476    }
477}
478
479/// One `timing_check_event` argument (LRM 31.3): a terminal, its optional
480/// edge control, and its optional `&&& condition` (LRM 31.5/31.7).
481#[derive(Clone, Debug, PartialEq, Eq)]
482pub struct CheckEvent {
483    /// The signal (and bit window) the event watches.
484    pub terminal: TerminalRef,
485    /// `posedge`/`negedge` (`EdgeKind::PosEdge`/`NegEdge`), a bare `edge` or
486    /// `edge [descriptors]` (`EdgeKind::BothEdges`), or [`EdgeKind::None`]
487    /// for an event with no edge control (any value change).
488    pub edge: sv_lang::EdgeKind,
489    /// The `edge [01, 1x]` descriptor list as written (LRM Table 31-14),
490    /// empty for every other form. Kept for diagnostics; the check process
491    /// reads [`Self::edge_control`], which is this resolved.
492    pub edge_descriptors: Vec<String>,
493    /// [`Self::edge`] and [`Self::edge_descriptors`] resolved to the
494    /// transitions that are this event (LRM 31.5), validated at discovery.
495    pub edge_control: EdgeControl,
496    /// The `&&&` qualifying condition (LRM 31.5/31.7), if any.
497    pub condition: Option<CondNode>,
498}
499
500/// A timing check limit / threshold / offset argument. Discovery records only
501/// WHERE it is (`arg_index`, the position in the check's own argument list);
502/// its VALUE needs `sv_lang::EvalSession` plus the timescale, neither
503/// reachable from this crate, so `sv-sim-bin::specify::discover` evaluates it
504/// and attaches [`Self::ticks`] -- the SAME "discover the SHAPE, attach the
505/// VALUE" split [`PathEntry::delays`] documents. `$sdf_annotate` (Track C)
506/// overwrites `ticks` in place afterwards. Signed: LRM 31.9 allows negative
507/// `$setuphold`/`$recrem` limits (and `$nochange` offsets).
508#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
509pub struct CheckLimit {
510    /// The argument's position in the check's own argument list (LRM Tables
511    /// 31-1..31-12 numbering, zero-based).
512    pub arg_index: u32,
513    /// The limit in scheduler ticks; `0` until `sv-sim-bin` attaches it.
514    pub ticks: i64,
515}
516
517/// The arguments shared by the five checks that take two events, one limit
518/// and an optional notifier -- `$setup` (LRM 31.3.1), `$hold` (31.3.2),
519/// `$recovery`, `$removal` (31.4), `$skew` (31.4.1). `$setup` alone lists its
520/// DATA event first; the others list the reference event first. Decoding
521/// puts each event in its own named field either way.
522#[derive(Clone, Debug, PartialEq, Eq)]
523pub struct EventPairArgs {
524    pub reference: CheckEvent,
525    pub data: CheckEvent,
526    pub limit: CheckLimit,
527    pub notifier: Option<TerminalRef>,
528}
529
530/// `$setuphold` (LRM 31.3.3, Syntax 31-5): all nine positions.
531#[derive(Clone, Debug, PartialEq, Eq)]
532pub struct SetupHoldArgs {
533    pub reference: CheckEvent,
534    pub data: CheckEvent,
535    pub setup_limit: CheckLimit,
536    pub hold_limit: CheckLimit,
537    pub notifier: Option<TerminalRef>,
538    /// `timestamp_condition` (position 5), if given.
539    pub timestamp_cond: Option<CondNode>,
540    /// `timecheck_condition` (position 6), if given.
541    pub timecheck_cond: Option<CondNode>,
542    /// `delayed_reference` (position 7), if given.
543    pub delayed_reference: Option<TerminalRef>,
544    /// `delayed_data` (position 8), if given.
545    pub delayed_data: Option<TerminalRef>,
546}
547
548/// `$recrem` (LRM 31.4.3, Syntax 31-8): the nine positions of
549/// [`SetupHoldArgs`], with the limits named for their meaning (position 2 is
550/// the RECOVERY limit, position 3 the REMOVAL limit).
551#[derive(Clone, Debug, PartialEq, Eq)]
552pub struct RecRemArgs {
553    pub reference: CheckEvent,
554    pub data: CheckEvent,
555    pub recovery_limit: CheckLimit,
556    pub removal_limit: CheckLimit,
557    pub notifier: Option<TerminalRef>,
558    pub timestamp_cond: Option<CondNode>,
559    pub timecheck_cond: Option<CondNode>,
560    pub delayed_reference: Option<TerminalRef>,
561    pub delayed_data: Option<TerminalRef>,
562}
563
564/// `$timeskew` (LRM 31.4.2, Syntax 31-10). A flag is `None` when elided, so
565/// the LRM default is applied in exactly one place (the check's behavior).
566#[derive(Clone, Debug, PartialEq, Eq)]
567pub struct TimeSkewArgs {
568    pub reference: CheckEvent,
569    pub data: CheckEvent,
570    pub limit: CheckLimit,
571    pub notifier: Option<TerminalRef>,
572    pub event_based: Option<bool>,
573    pub remain_active: Option<bool>,
574}
575
576/// `$fullskew` (LRM 31.4.3, Syntax 31-11).
577#[derive(Clone, Debug, PartialEq, Eq)]
578pub struct FullSkewArgs {
579    pub reference: CheckEvent,
580    pub data: CheckEvent,
581    /// `timing_check_limit` (position 2): "the maximum time by which the
582    /// data event should FOLLOW the reference event" -- i.e. the limit that
583    /// applies when the reference event comes first.
584    pub reference_first_limit: CheckLimit,
585    /// The second limit (position 3): "the maximum time by which the
586    /// reference event should follow the data event" -- applies when the
587    /// data event comes first.
588    pub data_first_limit: CheckLimit,
589    pub notifier: Option<TerminalRef>,
590    pub event_based: Option<bool>,
591    pub remain_active: Option<bool>,
592}
593
594/// `$period` (LRM 31.4.5, Syntax 31-13).
595#[derive(Clone, Debug, PartialEq, Eq)]
596pub struct PeriodArgs {
597    pub reference: CheckEvent,
598    pub limit: CheckLimit,
599    pub notifier: Option<TerminalRef>,
600}
601
602/// `$width` (LRM 31.4.4, Syntax 31-12). `threshold` is `None` when elided
603/// (LRM: "the default value for the threshold [is] zero").
604#[derive(Clone, Debug, PartialEq, Eq)]
605pub struct WidthArgs {
606    pub reference: CheckEvent,
607    pub limit: CheckLimit,
608    pub threshold: Option<CheckLimit>,
609    pub notifier: Option<TerminalRef>,
610}
611
612/// `$nochange` (LRM 31.4.6, Syntax 31-14).
613#[derive(Clone, Debug, PartialEq, Eq)]
614pub struct NoChangeArgs {
615    pub reference: CheckEvent,
616    pub data: CheckEvent,
617    pub start_edge_offset: CheckLimit,
618    pub end_edge_offset: CheckLimit,
619    pub notifier: Option<TerminalRef>,
620}
621
622/// A system timing check's arguments, decoded by the positional layout of
623/// LRM 1800-2023 Tables 31-1..31-12 -- one variant per check kind.
624#[derive(Clone, Debug, PartialEq, Eq)]
625pub enum CheckArgs {
626    Setup(EventPairArgs),
627    Hold(EventPairArgs),
628    SetupHold(SetupHoldArgs),
629    Recovery(EventPairArgs),
630    Removal(EventPairArgs),
631    RecRem(RecRemArgs),
632    Skew(EventPairArgs),
633    TimeSkew(TimeSkewArgs),
634    FullSkew(FullSkewArgs),
635    Period(PeriodArgs),
636    Width(WidthArgs),
637    NoChange(NoChangeArgs),
638}
639
640impl CheckArgs {
641    /// Every limit / threshold / offset this check carries, in argument
642    /// order -- the slots `sv-sim-bin` attaches tick values to and
643    /// `$sdf_annotate` (Track C) overwrites.
644    pub fn limits_mut(&mut self) -> Vec<&mut CheckLimit> {
645        match self {
646            CheckArgs::Setup(a)
647            | CheckArgs::Hold(a)
648            | CheckArgs::Recovery(a)
649            | CheckArgs::Removal(a)
650            | CheckArgs::Skew(a) => vec![&mut a.limit],
651            CheckArgs::SetupHold(a) => vec![&mut a.setup_limit, &mut a.hold_limit],
652            CheckArgs::RecRem(a) => vec![&mut a.recovery_limit, &mut a.removal_limit],
653            CheckArgs::TimeSkew(a) => vec![&mut a.limit],
654            CheckArgs::FullSkew(a) => {
655                vec![&mut a.reference_first_limit, &mut a.data_first_limit]
656            }
657            CheckArgs::Period(a) => vec![&mut a.limit],
658            CheckArgs::Width(a) => {
659                let mut v = vec![&mut a.limit];
660                v.extend(a.threshold.as_mut());
661                v
662            }
663            CheckArgs::NoChange(a) => vec![&mut a.start_edge_offset, &mut a.end_edge_offset],
664        }
665    }
666}
667
668/// A `SystemTimingCheck` (LRM §31.2), decoded.
669#[derive(Clone, Debug, PartialEq, Eq)]
670pub struct CheckEntry {
671    /// The instance the check's declaration lives in.
672    pub instance: InstanceId,
673    /// The `SystemTimingCheck` symbol's own identity, so `sv-sim-bin` can
674    /// re-locate the symbol to evaluate [`CheckLimit`] values (see its doc).
675    pub check_uid: SymbolId,
676    pub kind: sv_lang::SystemTimingCheckKind,
677    pub args: CheckArgs,
678}
679
680/// The per-design, runtime-mutable timing-annotation table (§1.1).
681#[derive(Default)]
682pub struct TimingModel {
683    specparams: HashMap<(InstanceId, SymbolId), ConstantValue>,
684    /// One per desugared [`PathEntry`] (Track A).
685    pub paths: Vec<PathEntry>,
686    /// One per [`CheckEntry`] (Track B).
687    pub timing_checks: Vec<CheckEntry>,
688    /// The pulse style invocation option (LRM 30.7.4.1: "The pulse style invocation options take
689    /// precedence over pulse style specify block declarations"): `Some` overrides every
690    /// `pulsestyle_*` declaration, `None` (no option given) leaves the declarations in force.
691    pulse_style_option: Option<PulseStyle>,
692    /// The `showcancelled` invocation option, the same way (LRM 30.7.4.2: "The showcancelled
693    /// invocation options take precedence over the showcancelled specify block declarations").
694    showcancelled_option: Option<bool>,
695}
696
697impl TimingModel {
698    /// An empty table (no design has been seeded into it yet).
699    pub fn new() -> TimingModel {
700        TimingModel::default()
701    }
702
703    /// Seeds the specparam table from every `Specparam` member of each
704    /// `(instance, body)` pair in `infos` — the SAME per-instance
705    /// `(InstanceId, Symbol)` shape `sv-sim-bin::primitives::plan`'s own
706    /// `bodies` parameter already uses — via [`super::module_items`] (the
707    /// same discovery surface `primitives.rs` already walks). `Specparam`
708    /// is a direct module member, legal both inside and outside a `specify`
709    /// block (LRM 30.3). Stores each one's frozen `constant_value()` — the
710    /// pre-SDF baseline a later `$sdf_annotate` `LABEL` annotation
711    /// (Task 28) overwrites in place through [`Self::set_specparam`].
712    ///
713    /// A `specparam` declared INSIDE a `specify ... endspecify` block (LRM
714    /// 30.3's own `specify_item` production lists `specparam_declaration`;
715    /// 30.7.1's `PATHPULSE$` is only ever legal there) is not a direct
716    /// `Specparam` member of `body` the way a module-scope one is:
717    /// `module_items` deliberately leaves `SpecifyBlock` opaque (it only
718    /// recurses into `GenerateBlock`/`GenerateBlockArray`, per that
719    /// function's own doc comment — dedup.rs/elaborate.rs/instance.rs/
720    /// primitives.rs all rely on that), so a `specify`-block specparam's
721    /// real `Specparam` symbol is a child of the `SpecifyBlock` member
722    /// itself, never surfaced by the `module_items` walk above. This loop
723    /// therefore also looks inside every `SpecifyBlock` member it finds and
724    /// seeds any `Specparam` declared directly in it, one level deep (a
725    /// `specify` block cannot nest another one — 30.3 gives it no such
726    /// production — so one level is exhaustive).
727    ///
728    /// Deviation from the plan's own pseudocode signature: no `&EvalSession`
729    /// parameter. `Symbol::constant_value()` is documented as "a pure read
730    /// on a frozen design... never evaluates" (sv-lang's own doc comment),
731    /// so no session is needed here — the plan carried one for symmetry
732    /// with `specify_eval::eval_specparam_aware`'s (which genuinely needs
733    /// one, for its general-constant-folding fallback), not because seeding
734    /// itself requires one.
735    pub fn seed_specparams(&mut self, infos: &[(InstanceId, Symbol<'_>)]) {
736        for &(id, body) in infos {
737            for m in super::module_items(&body) {
738                if m.kind() == SymbolKind::Specparam {
739                    if let Some(v) = m.constant_value() {
740                        self.specparams.insert((id, m.uid()), v);
741                    }
742                    continue;
743                }
744                if m.kind() != SymbolKind::SpecifyBlock {
745                    continue;
746                }
747                for inner in m.members() {
748                    if inner.kind() != SymbolKind::Specparam {
749                        continue;
750                    }
751                    if let Some(v) = inner.constant_value() {
752                        self.specparams.insert((id, inner.uid()), v);
753                    }
754                }
755            }
756        }
757    }
758
759    /// The specparam identified by `(instance, uid)`'s current value: the
760    /// pre-SDF baseline [`seed_specparams`](Self::seed_specparams) stored,
761    /// or whatever a later direct mutation ([`set_specparam`]
762    /// (Self::set_specparam) — `$sdf_annotate`, Task 28) has since
763    /// overwritten it with. A plain table lookup, needing no
764    /// `EvalSession`/live `Design`.
765    pub fn specparam(&self, instance: InstanceId, uid: &SymbolId) -> Option<&ConstantValue> {
766        self.specparams.get(&(instance, uid.clone()))
767    }
768
769    /// Overwrites (or inserts) a specparam's value directly — what Task 28's
770    /// real SDF `LABEL` annotation calls, and what this task's own unit
771    /// tests use to simulate it ahead of time. This task's "mutation path"
772    /// acceptance criterion: no `EvalSession`/`Design`/`Symbol` anywhere in
773    /// this signature, only `InstanceId`/`SymbolId`/the plain, fully-owned
774    /// `ConstantValue`.
775    pub fn set_specparam(&mut self, instance: InstanceId, uid: SymbolId, value: ConstantValue) {
776        self.specparams.insert((instance, uid), value);
777    }
778
779    /// Attaches `delays` to every desugared [`PathEntry`] that came from
780    /// the one `TimingPath` symbol identified by `(instance, path_uid)`
781    /// (LRM 30.4.6: several entries share one declaration, and all share
782    /// its one delay list) -- `sv-sim-bin`'s own plan-time pass
783    /// (`crate::specify::discover`, right after calling [`Self::discover`])
784    /// is the only caller; see [`PathEntry::delays`]'s own doc comment for
785    /// why evaluation lives there, not here. A no-op, never an error, if
786    /// nothing matches -- [`Self::discover`]'s own loop already guarantees
787    /// that a `path_uid` it produced has at least one entry, so a caller
788    /// passing a `path_uid` it did not itself get from THIS SAME
789    /// `TimingModel`'s own [`Self::paths`] is a caller bug, not a runtime
790    /// condition this method needs to report.
791    pub fn set_path_delays(
792        &mut self,
793        instance: InstanceId,
794        path_uid: &SymbolId,
795        delays: PathDelaySet,
796    ) {
797        for p in &mut self.paths {
798            if p.instance == instance && &p.path_uid == path_uid {
799                p.delays = delays;
800                // LRM 30.7: absent a `PATHPULSE$`, both limits default to
801                // each transition's own delay. Resetting them HERE (rather
802                // than leaving them a separately-stored default) is what
803                // makes a path whose delays are re-annotated keep
804                // following its delay; a `PATHPULSE$` must therefore be
805                // applied AFTER the delays (`set_path_pulse_limits`).
806                p.reject_limit = delays;
807                p.error_limit = delays;
808            }
809        }
810    }
811
812    /// Attaches a `PATHPULSE$` (LRM 30.7.1) -- `reject` and `error`, already
813    /// evaluated and tick-converted by the caller exactly like
814    /// [`Self::set_path_delays`]'s own delays (see that method for why
815    /// evaluation lives in `sv-sim-bin`) -- to every desugared [`PathEntry`]
816    /// of the one `TimingPath` symbol `(instance, path_uid)`; every
817    /// transition of each gets the same two values.
818    ///
819    /// **Implementation-defined choice**: LRM 30.7.1 requires the error limit
820    /// to be at least the reject limit but does not say what a source that
821    /// writes `(6, 2)` means. Rather than reject a design over it, the error
822    /// limit is raised to the reject limit (`error = max(error, reject)`),
823    /// which makes the region between the two empty: a pulse is then either
824    /// cancelled or passed, never filtered to `x`.
825    ///
826    /// Like [`Self::set_path_delays`], a no-op if nothing matches.
827    pub fn set_path_pulse_limits(
828        &mut self,
829        instance: InstanceId,
830        path_uid: &SymbolId,
831        reject: u64,
832        error: u64,
833    ) {
834        let error = error.max(reject);
835        for p in &mut self.paths {
836            if p.instance == instance && &p.path_uid == path_uid {
837                p.reject_limit = PathDelaySet::uniform(reject);
838                p.error_limit = PathDelaySet::uniform(error);
839            }
840        }
841    }
842
843    /// Sets every path's reject and error limit to `reject_pct` / `error_pct` percent of each
844    /// transition's OWN delay (LRM 30.7.2, 32.7: the global invocation options), rounded to the
845    /// nearest tick, an error limit below the reject limit raised to it (the same
846    /// implementation-defined repair as [`Self::set_path_pulse_limits`]). `100.0` is exact (the
847    /// LRM default, limits equal to the delays). A `PATHPULSE$` is more specific than this
848    /// option and must be applied AFTER it ([`Self::set_path_pulse_limits`]); like
849    /// [`Self::set_path_delays`], which re-establishes the default this replaces, it must come
850    /// after the delays.
851    pub fn apply_pulse_percent(&mut self, reject_pct: f64, error_pct: f64) {
852        let scale = |v: u64, pct: f64| -> u64 {
853            if pct == 100.0 {
854                v
855            } else {
856                // A saturating float-to-int cast: a tick count beyond 2^53 loses low bits first,
857                // an absurd delay for a pulse limit.
858                (v as f64 * pct / 100.0).round() as u64
859            }
860        };
861        for p in &mut self.paths {
862            let delays = p.delays.transitions();
863            let reject = delays.map(|d| scale(d, reject_pct));
864            let error: [u64; 12] =
865                std::array::from_fn(|i| scale(delays[i], error_pct).max(reject[i]));
866            p.reject_limit = PathDelaySet::from_transitions(reject);
867            p.error_limit = PathDelaySet::from_transitions(error);
868        }
869    }
870
871    /// Sets the pulse style and `showcancelled` invocation options (LRM 30.7.4.1/30.7.4.2). An
872    /// option that is `Some` takes precedence over every specify block declaration of its axis
873    /// (the LRM's own wording), so [`Self::discover`] applies it AFTER the declarations; one that
874    /// is `None` was not given and the declarations (then the built-in on-event /
875    /// `noshowcancelled`) decide. Must be called BEFORE [`Self::discover`].
876    pub fn set_pulse_options(&mut self, style: Option<PulseStyle>, showcancelled: Option<bool>) {
877        self.pulse_style_option = style;
878        self.showcancelled_option = showcancelled;
879    }
880
881    /// Walks every `(instance, body)` pair's `SpecifyBlock` members
882    /// (Task 3, `docs/superpowers/plans/2026-10-03-sdf.md`), appending one
883    /// [`PathEntry`] per desugared module path to [`Self::paths`] and one
884    /// [`CheckEntry`] per `SystemTimingCheck` to [`Self::timing_checks`]
885    /// ([`discover_check`] decodes its arguments by kind and enforces the
886    /// clause's compile-time restrictions as named errors, Task 13).
887    ///
888    /// `SpecifyBlock`'s own member-kind match is exhaustive over LRM 30.3's
889    /// `specify_item` production (`specparam_declaration` |
890    /// `path_declaration` | `system_timing_check` |
891    /// `pulsestyle_declaration`/`showcancelled_declaration`, i.e.
892    /// `SymbolKind::{Specparam,TimingPath,SystemTimingCheck,PulseStyle}`):
893    /// any other member kind is a named [`ElabError::UnsupportedConstruct`],
894    /// never a silent skip, matching this crate's own top-of-file
895    /// discipline. `Specparam` members are skipped here (already seeded by
896    /// [`Self::seed_specparams`]); `PulseStyle` members (Task 11:
897    /// `pulsestyle_onevent`/`_ondetect`/`showcancelled`/`noshowcancelled`) are
898    /// validated against LRM 30.7.4.1/30.7.4.2's declaration-order rule as
899    /// they are met, then applied to the `PathEntry`s whose outputs they name
900    /// once the module's whole specify content has been walked
901    /// ([`discover_pulse_style`], [`apply_pulse_style_declarations`]).
902    ///
903    /// A `TimingPath` whose desugaring would need [`EvalSession::
904    /// eval_constant`] (a bit-select/part-select terminal whose index is
905    /// not already folded) is why this method takes `eval`, unlike
906    /// [`Self::seed_specparams`] — mirrors `specify_eval::
907    /// eval_specparam_aware`'s own reason for carrying one.
908    pub fn discover<'d>(
909        &mut self,
910        eval: &EvalSession<'d>,
911        infos: &[(InstanceId, Symbol<'d>)],
912    ) -> Result<(), ElabError> {
913        // Needed only by `syntax_terminal_counts` (the sibling-terminal-drop
914        // check `discover_path` runs after sv-lang's own per-terminal LRM
915        // 30.4.1 filtering) -- computed once here, mirroring `run.rs`'s own
916        // single `eval.design().syntax_trees()` call reused across every
917        // instance, rather than once per path.
918        let trees = eval.design().syntax_trees();
919        for &(id, body) in infos {
920            let ports = port_directions(&body);
921            // Task 11: every pulse style / showcancelled declaration of this
922            // instance, in source order across all its specify blocks; applied
923            // to the paths once the whole module has been walked.
924            let mut declarations: Vec<(TerminalRef, PulseStyleKind)> = Vec::new();
925            for m in super::module_items(&body) {
926                if m.kind() != SymbolKind::SpecifyBlock {
927                    continue;
928                }
929                for inner in m.members() {
930                    match inner.kind() {
931                        SymbolKind::Specparam => {}
932                        SymbolKind::TimingPath => {
933                            self.paths
934                                .extend(discover_path(eval, id, &inner, &ports, &trees)?);
935                        }
936                        SymbolKind::PulseStyle => {
937                            declarations.extend(discover_pulse_style(
938                                eval,
939                                id,
940                                &inner,
941                                &ports,
942                                &self.paths,
943                            )?);
944                        }
945                        SymbolKind::SystemTimingCheck => {
946                            self.timing_checks.push(discover_check(eval, id, &inner)?);
947                        }
948                        other => {
949                            return Err(ElabError::UnsupportedConstruct {
950                                hier_path: inner.hierarchical_path(),
951                                detail: format!(
952                                    "specify block member of kind `{other:?}` (LRM 30.3's own \
953                                     specify_item production only allows specparam_declaration, \
954                                     path_declaration, system_timing_check and \
955                                     pulsestyle_declaration/showcancelled_declaration — this is \
956                                     an unexpected AST shape, not a construct this task has \
957                                     chosen to defer)"
958                                ),
959                            });
960                        }
961                    }
962                }
963            }
964            // Task 8: LRM 30.4.4.3's "same destination reference shape"
965            // and 30.4.4.4's "ifnone + unconditional for the same path"
966            // validations, run once every one of `id`'s own specify
967            // blocks has contributed its `PathEntry`s (several specify
968            // blocks per module are legal, so this runs after the whole
969            // `module_items` loop above, not per `SpecifyBlock` member).
970            validate_state_dependent_groups(id, &self.paths)?;
971            apply_pulse_style_declarations(id, &mut self.paths, &declarations);
972            // LRM 30.7.4.1/30.7.4.2: the invocation options beat the declarations.
973            for p in self.paths.iter_mut().filter(|p| p.instance == id) {
974                if let Some(style) = self.pulse_style_option {
975                    p.pulse_style = style;
976                }
977                if let Some(show) = self.showcancelled_option {
978                    p.showcancelled = show;
979                }
980            }
981        }
982        Ok(())
983    }
984}
985
986/// Strips the wrappers slang puts around a timing check's signal arguments:
987/// an implicit `Conversion`, and the `Assignment` slang builds for an
988/// output-direction formal (the notifier and the two delayed signals are bound
989/// as output arguments), whose `left()` is the actual the user wrote.
990fn strip_arg_wrappers<'d>(mut e: Expression<'d>) -> Expression<'d> {
991    // Equality tests rather than a `match`: this is not a dispatch over the
992    // kind enum (every other kind is "not a wrapper", by design), and the
993    // kind guard forbids a silent wildcard arm over one.
994    loop {
995        let k = e.kind();
996        let next = if k == ExpressionKind::Conversion {
997            e.operand()
998        } else if k == ExpressionKind::Assignment {
999            e.left()
1000        } else {
1001            None
1002        };
1003        match next {
1004            Some(n) => e = n,
1005            None => return e,
1006        }
1007    }
1008}
1009
1010/// A timing check's argument list, with the per-position accessors
1011/// [`discover_check`] decodes through. Positions are zero-based and match LRM
1012/// Tables 31-1..31-12's argument order; an elided or never-written trailing
1013/// argument reads as absent either way.
1014struct CheckArgsView<'a, 'd> {
1015    eval: &'a EvalSession<'d>,
1016    check: &'a Symbol<'d>,
1017    name: &'static str,
1018    args: Vec<sv_lang::SystemTimingCheckArg<'d>>,
1019}
1020
1021impl<'a, 'd> CheckArgsView<'a, 'd> {
1022    fn err(&self, detail: String) -> ElabError {
1023        unsupported(self.check, format!("`{}` {detail}", self.name))
1024    }
1025
1026    fn arg(&self, i: usize) -> Option<&sv_lang::SystemTimingCheckArg<'d>> {
1027        self.args.get(i).filter(|a| a.expr.is_some())
1028    }
1029
1030    fn require(
1031        &self,
1032        i: usize,
1033        what: &str,
1034    ) -> Result<&sv_lang::SystemTimingCheckArg<'d>, ElabError> {
1035        self.arg(i).ok_or_else(|| {
1036            self.err(format!(
1037                "is missing its required {what} (argument {}; an empty argument is not allowed \
1038                 there)",
1039                i + 1
1040            ))
1041        })
1042    }
1043
1044    /// A signal-valued argument (an event terminal, a notifier, a delayed
1045    /// signal): a net or variable, whole or a constant bit/part window.
1046    fn signal_of(&self, e: Expression<'d>, what: &str) -> Result<TerminalRef, ElabError> {
1047        let e = strip_arg_wrappers(e);
1048        let (sym, bits) = resolve_cond_signal_ref(self.eval, self.check, e)?;
1049        // LRM 25.6: a signal of an interface port (`ch.d`) is a terminal.
1050        let modport_signal = sym.kind() == SymbolKind::ModportPort
1051            && sym.modport_port_explicit_connection().is_none()
1052            && sym
1053                .modport_port_internal_symbol()
1054                .is_some_and(|s| matches!(s.kind(), SymbolKind::Net | SymbolKind::Variable));
1055        if !modport_signal && !matches!(sym.kind(), SymbolKind::Net | SymbolKind::Variable) {
1056            return Err(self.err(format!(
1057                "{what} `{}` is a {:?}, not a net or variable",
1058                sym.name(),
1059                sym.kind()
1060            )));
1061        }
1062        Ok((sym.uid(), bits))
1063    }
1064
1065    fn event(&self, i: usize, what: &str) -> Result<CheckEvent, ElabError> {
1066        let a = self.require(i, what)?;
1067        let expr = a
1068            .expr
1069            .expect("require() only returns arguments with an expr");
1070        let condition = a
1071            .condition
1072            .map(|c| compile_cond_expr_in(self.eval, self.check, c, CondDialect::TimingCheck))
1073            .transpose()?;
1074        let edge_control = EdgeControl::from_syntax(a.edge, &a.edge_descriptors)
1075            .map_err(|e| self.err(format!("{what}: {e}")))?;
1076        Ok(CheckEvent {
1077            terminal: self.signal_of(expr, what)?,
1078            edge: a.edge,
1079            edge_descriptors: a.edge_descriptors.clone(),
1080            edge_control,
1081            condition,
1082        })
1083    }
1084
1085    fn limit(&self, i: usize, what: &str) -> Result<CheckLimit, ElabError> {
1086        self.require(i, what)?;
1087        Ok(CheckLimit {
1088            arg_index: i as u32,
1089            ticks: 0,
1090        })
1091    }
1092
1093    fn opt_limit(&self, i: usize) -> Option<CheckLimit> {
1094        self.arg(i).map(|_| CheckLimit {
1095            arg_index: i as u32,
1096            ticks: 0,
1097        })
1098    }
1099
1100    fn opt_signal(&self, i: usize, what: &str) -> Result<Option<TerminalRef>, ElabError> {
1101        self.arg(i)
1102            .map(|a| self.signal_of(a.expr.expect("arg() filters on expr"), what))
1103            .transpose()
1104    }
1105
1106    fn opt_cond(&self, i: usize) -> Result<Option<CondNode>, ElabError> {
1107        self.arg(i)
1108            .map(|a| {
1109                compile_cond_expr_in(
1110                    self.eval,
1111                    self.check,
1112                    a.expr.expect("arg() filters on expr"),
1113                    CondDialect::TimingCheck,
1114                )
1115            })
1116            .transpose()
1117    }
1118
1119    fn opt_flag(&self, i: usize, what: &str) -> Result<Option<bool>, ElabError> {
1120        self.arg(i)
1121            .map(|a| {
1122                const_i64(self.eval, a.expr.expect("arg() filters on expr"))
1123                    .map(|v| v != 0)
1124                    .ok_or_else(|| self.err(format!("{what} must be a constant expression")))
1125            })
1126            .transpose()
1127    }
1128
1129    fn pair(&self, data_first: bool) -> Result<EventPairArgs, ElabError> {
1130        let (r, d) = if data_first { (1, 0) } else { (0, 1) };
1131        Ok(EventPairArgs {
1132            reference: self.event(r, "reference event")?,
1133            data: self.event(d, "data event")?,
1134            limit: self.limit(2, "limit")?,
1135            notifier: self.opt_signal(3, "notifier")?,
1136        })
1137    }
1138}
1139
1140/// Decodes one `SystemTimingCheck` symbol (LRM 31.2-31.6) into a typed
1141/// [`CheckEntry`], by the hard-coded positional layout of Tables 31-1..31-12
1142/// (`$setup` alone lists its data event first; `$setuphold`/`$recrem` carry
1143/// nine positions; `$skew` family flags and `$width`'s threshold are
1144/// optional). slang already diagnoses most malformed calls, but the CLI's
1145/// "the design has errors" abort is the only thing between a violation and
1146/// this function, so the clause's compile-time restrictions are re-raised here
1147/// as named errors: `$width`/`$period` need an edge specification (31.4.4,
1148/// 31.4.5), `$nochange`'s reference event must be `posedge`/`negedge` (31.4.6),
1149/// and `$width`'s notifier needs a non-null, constant threshold (31.4.4).
1150fn discover_check<'d>(
1151    eval: &EvalSession<'d>,
1152    instance: InstanceId,
1153    check: &Symbol<'d>,
1154) -> Result<CheckEntry, ElabError> {
1155    use sv_lang::SystemTimingCheckKind as K;
1156    let kind = check.system_timing_check_kind();
1157    let name = match kind {
1158        K::Setup => "$setup",
1159        K::Hold => "$hold",
1160        K::SetupHold => "$setuphold",
1161        K::Recovery => "$recovery",
1162        K::Removal => "$removal",
1163        K::RecRem => "$recrem",
1164        K::Skew => "$skew",
1165        K::TimeSkew => "$timeskew",
1166        K::FullSkew => "$fullskew",
1167        K::Period => "$period",
1168        K::Width => "$width",
1169        K::NoChange => "$nochange",
1170        K::Unknown => {
1171            return Err(unsupported(
1172                check,
1173                "system timing check of an unrecognized kind (unexpected AST shape)".to_string(),
1174            ));
1175        }
1176    };
1177    let v = CheckArgsView {
1178        eval,
1179        check,
1180        name,
1181        args: check.system_timing_check_arguments().collect(),
1182    };
1183    let args = match kind {
1184        // Already rejected by the name table above.
1185        K::Unknown => {
1186            return Err(unsupported(
1187                check,
1188                "system timing check of an unrecognized kind (unexpected AST shape)".to_string(),
1189            ));
1190        }
1191        K::Setup => CheckArgs::Setup(v.pair(true)?),
1192        K::Hold => CheckArgs::Hold(v.pair(false)?),
1193        K::Recovery => CheckArgs::Recovery(v.pair(false)?),
1194        K::Removal => CheckArgs::Removal(v.pair(false)?),
1195        K::Skew => CheckArgs::Skew(v.pair(false)?),
1196        K::SetupHold => CheckArgs::SetupHold(SetupHoldArgs {
1197            reference: v.event(0, "reference event")?,
1198            data: v.event(1, "data event")?,
1199            setup_limit: v.limit(2, "setup limit")?,
1200            hold_limit: v.limit(3, "hold limit")?,
1201            notifier: v.opt_signal(4, "notifier")?,
1202            timestamp_cond: v.opt_cond(5)?,
1203            timecheck_cond: v.opt_cond(6)?,
1204            delayed_reference: v.opt_signal(7, "delayed reference")?,
1205            delayed_data: v.opt_signal(8, "delayed data")?,
1206        }),
1207        K::RecRem => CheckArgs::RecRem(RecRemArgs {
1208            reference: v.event(0, "reference event")?,
1209            data: v.event(1, "data event")?,
1210            recovery_limit: v.limit(2, "recovery limit")?,
1211            removal_limit: v.limit(3, "removal limit")?,
1212            notifier: v.opt_signal(4, "notifier")?,
1213            timestamp_cond: v.opt_cond(5)?,
1214            timecheck_cond: v.opt_cond(6)?,
1215            delayed_reference: v.opt_signal(7, "delayed reference")?,
1216            delayed_data: v.opt_signal(8, "delayed data")?,
1217        }),
1218        K::TimeSkew => CheckArgs::TimeSkew(TimeSkewArgs {
1219            reference: v.event(0, "reference event")?,
1220            data: v.event(1, "data event")?,
1221            limit: v.limit(2, "limit")?,
1222            notifier: v.opt_signal(3, "notifier")?,
1223            event_based: v.opt_flag(4, "event_based_flag")?,
1224            remain_active: v.opt_flag(5, "remain_active_flag")?,
1225        }),
1226        K::FullSkew => CheckArgs::FullSkew(FullSkewArgs {
1227            reference: v.event(0, "reference event")?,
1228            data: v.event(1, "data event")?,
1229            reference_first_limit: v.limit(2, "first limit")?,
1230            data_first_limit: v.limit(3, "second limit")?,
1231            notifier: v.opt_signal(4, "notifier")?,
1232            event_based: v.opt_flag(5, "event_based_flag")?,
1233            remain_active: v.opt_flag(6, "remain_active_flag")?,
1234        }),
1235        K::Period => {
1236            let reference = v.event(0, "reference event")?;
1237            if reference.edge == sv_lang::EdgeKind::None {
1238                return Err(v.err(
1239                    "reference event must be an edge specification (`posedge`, `negedge` or \
1240                     `edge`) -- LRM 31.4.5: the period is measured between successive \
1241                     occurrences of that edge"
1242                        .to_string(),
1243                ));
1244            }
1245            CheckArgs::Period(PeriodArgs {
1246                reference,
1247                limit: v.limit(1, "limit")?,
1248                notifier: v.opt_signal(2, "notifier")?,
1249            })
1250        }
1251        K::Width => {
1252            let reference = v.event(0, "reference event")?;
1253            if reference.edge == sv_lang::EdgeKind::None {
1254                return Err(v.err(
1255                    "reference event must be an edge specification (`posedge`, `negedge` or \
1256                     `edge`) -- LRM 31.4.4: the width is measured from that edge to the opposite \
1257                     one"
1258                    .to_string(),
1259                ));
1260            }
1261            if v.arg(3).is_some() && v.arg(2).is_none() {
1262                return Err(v.err(
1263                    "has a notifier but a null threshold: LRM 31.4.4 allows the notifier only \
1264                     when the threshold argument is also given (write `0` for no threshold)"
1265                        .to_string(),
1266                ));
1267            }
1268            if let Some(t) = v.arg(2) {
1269                let te = t.expr.expect("arg() filters on expr");
1270                if te
1271                    .constant_value()
1272                    .or_else(|| eval.eval_constant(te))
1273                    .is_none()
1274                {
1275                    return Err(v.err(
1276                        "threshold must be a constant expression (LRM 31.4.4) -- a notifier in \
1277                         the third position is the classic mistake; the notifier is the fourth \
1278                         argument"
1279                            .to_string(),
1280                    ));
1281                }
1282            }
1283            CheckArgs::Width(WidthArgs {
1284                reference,
1285                limit: v.limit(1, "limit")?,
1286                threshold: v.opt_limit(2),
1287                notifier: v.opt_signal(3, "notifier")?,
1288            })
1289        }
1290        K::NoChange => {
1291            let reference = v.event(0, "reference event")?;
1292            let plain_edge = matches!(
1293                reference.edge,
1294                sv_lang::EdgeKind::PosEdge | sv_lang::EdgeKind::NegEdge
1295            ) && reference.edge_descriptors.is_empty();
1296            if !plain_edge {
1297                return Err(v.err(
1298                    "reference event must be `posedge` or `negedge` (LRM 31.4.6); an `edge` \
1299                     control specifier (with or without a descriptor list) or a bare signal is \
1300                     not allowed"
1301                        .to_string(),
1302                ));
1303            }
1304            CheckArgs::NoChange(NoChangeArgs {
1305                reference,
1306                data: v.event(1, "data event")?,
1307                start_edge_offset: v.limit(2, "start edge offset")?,
1308                end_edge_offset: v.limit(3, "end edge offset")?,
1309                notifier: v.opt_signal(4, "notifier")?,
1310            })
1311        }
1312    };
1313    Ok(CheckEntry {
1314        instance,
1315        check_uid: check.uid(),
1316        kind,
1317        args,
1318    })
1319}
1320
1321/// Whether two output terminals of one instance share at least one bit
1322/// (`None` is the whole signal, `Some((lo, width))` a bit/part-select window).
1323fn terminals_overlap(a: &TerminalRef, b: &TerminalRef) -> bool {
1324    if a.0 != b.0 {
1325        return false;
1326    }
1327    match (a.1, b.1) {
1328        (Some((alo, aw)), Some((blo, bw))) => alo < blo + bw && blo < alo + aw,
1329        _ => true,
1330    }
1331}
1332
1333/// One `PulseStyle` symbol (LRM 30.7.4: `pulsestyle_onevent`/
1334/// `pulsestyle_ondetect`/`showcancelled`/`noshowcancelled` followed by a
1335/// `list_of_path_outputs`): its outputs paired with the declaration kind, after
1336/// the 30.7.4.1/30.7.4.2 ordering rule — "it is an error if a module path
1337/// output appears in a [pulse style / showcancelled] declaration after it has
1338/// already appeared in a module path declaration" — has been checked against
1339/// `paths`, the path entries of every module path declared BEFORE this
1340/// declaration (discovery walks a module's specify blocks in source order, so
1341/// that is exactly `instance`'s entries so far). slang itself only warns
1342/// (`InvalidPulseStyle`) about a violation, so this is the hard error.
1343///
1344/// "Appeared" is overlap of the output's bit window: a declaration for `y[0]`
1345/// after a path to `y[1]` names a different output bit and is legal.
1346fn discover_pulse_style<'d>(
1347    eval: &EvalSession<'d>,
1348    instance: InstanceId,
1349    decl: &Symbol<'d>,
1350    ports: &HashMap<SymbolId, ArgumentDirection>,
1351    paths: &[PathEntry],
1352) -> Result<Vec<(TerminalRef, PulseStyleKind)>, ElabError> {
1353    let kind = decl.pulse_style_kind();
1354    let (keyword, clause) = match kind {
1355        PulseStyleKind::OnEvent => ("pulsestyle_onevent", "30.7.4.1"),
1356        PulseStyleKind::OnDetect => ("pulsestyle_ondetect", "30.7.4.1"),
1357        PulseStyleKind::ShowCancelled => ("showcancelled", "30.7.4.2"),
1358        PulseStyleKind::NoShowCancelled => ("noshowcancelled", "30.7.4.2"),
1359    };
1360    let mut out = Vec::new();
1361    for e in decl.pulse_style_terminals() {
1362        // Same resolution (and 30.4.1 output-direction check) as a module path
1363        // destination: a pulse style names module path outputs.
1364        let terminal = resolve_terminal(eval, decl, ports, e, false)?;
1365        let already_used = paths.iter().any(|p| {
1366            p.instance == instance && p.outputs.iter().any(|o| terminals_overlap(o, &terminal))
1367        });
1368        if already_used {
1369            let full = terminal.0.to_string();
1370            let name = full.rsplit('.').next().unwrap_or(&full);
1371            return Err(unsupported(
1372                decl,
1373                format!(
1374                    "`{keyword} {name}` follows a module path declaration that already names \
1375                     output `{name}` (LRM {clause}: it is an error for a module path output to \
1376                     appear in this declaration after it has already appeared in a module path \
1377                     declaration — the output would have contradictory pulse behavior for the \
1378                     earlier path)"
1379                ),
1380            ));
1381        }
1382        out.push((terminal, kind));
1383    }
1384    Ok(out)
1385}
1386
1387/// Applies `declarations` (source order) to `instance`'s `paths`: a declaration
1388/// reaches every path whose output window overlaps its terminal. The two
1389/// settings are independent axes — `pulsestyle_*` sets
1390/// [`PathEntry::pulse_style`], `showcancelled`/`noshowcancelled` sets
1391/// [`PathEntry::showcancelled`] — and **implementation-defined**: when one
1392/// output is declared twice on the same axis (legal, since both precede its
1393/// paths), the later declaration wins; the LRM only forbids the contradictory
1394/// order relative to a path.
1395fn apply_pulse_style_declarations(
1396    instance: InstanceId,
1397    paths: &mut [PathEntry],
1398    declarations: &[(TerminalRef, PulseStyleKind)],
1399) {
1400    for (terminal, kind) in declarations {
1401        for p in paths.iter_mut().filter(|p| {
1402            p.instance == instance && p.outputs.iter().any(|o| terminals_overlap(o, terminal))
1403        }) {
1404            match kind {
1405                PulseStyleKind::OnEvent => p.pulse_style = PulseStyle::OnEvent,
1406                PulseStyleKind::OnDetect => p.pulse_style = PulseStyle::OnDetect,
1407                PulseStyleKind::ShowCancelled => p.showcancelled = true,
1408                PulseStyleKind::NoShowCancelled => p.showcancelled = false,
1409            }
1410        }
1411    }
1412}
1413
1414/// `body`'s module ports, keyed by each port's internal net/variable's
1415/// [`SymbolUid::uid`] (the SAME `port_internal_symbol().unwrap_or(port)`
1416/// pattern `nets.rs`/`run.rs`/`primitives.rs` already use to relate a
1417/// port to the signal it connects to inside the module) — the table
1418/// [`validate_direction`] checks a specify-path terminal's own symbol
1419/// against for LRM 30.4.1's restriction.
1420fn port_directions<'d>(body: &Symbol<'d>) -> HashMap<SymbolId, ArgumentDirection> {
1421    let mut map = HashMap::new();
1422    for m in body.members() {
1423        if m.kind() != SymbolKind::Port {
1424            continue;
1425        }
1426        let internal = m.port_internal_symbol().unwrap_or(m);
1427        map.insert(internal.uid(), m.port_direction());
1428    }
1429    map
1430}
1431
1432/// `ElabError::UnsupportedConstruct` at `sym`'s own hierarchical path —
1433/// every named error this module raises goes through this one call site.
1434fn unsupported(sym: &Symbol<'_>, detail: String) -> ElabError {
1435    ElabError::UnsupportedConstruct {
1436        hier_path: sym.hierarchical_path(),
1437        detail,
1438    }
1439}
1440
1441/// `e`'s value as a compile-time constant `i64`, folding what slang left
1442/// unfolded (mirrors `sv-sim-bin::primitives::constant_of`/`sv-sim-bin::
1443/// specify_eval::constant_of` — a third, small copy for the same reason
1444/// `specify_eval.rs`'s own doc comment gives: the existing ones are
1445/// private `fn`s in sibling/downstream crates this crate cannot reach into
1446/// or be reached from).
1447fn const_i64<'d>(eval: &EvalSession<'d>, e: Expression<'d>) -> Option<i64> {
1448    e.constant_value()
1449        .or_else(|| eval.eval_constant(e))
1450        .and_then(|v| v.as_i64())
1451}
1452
1453/// LRM 30.4.1: a module path source must be a net connected to an
1454/// input/inout port; a destination must be a net or variable connected to
1455/// an output/inout port. Verified live against sv-lang=0.1.9/slang 11.0:
1456/// slang already enforces exactly this (its own diagnostic text literally
1457/// reads "specify path source/destination must be an 'input'/'output' or
1458/// 'inout' ...", `SpecifyBlockSymbol::checkPathTerminal`) and already drops
1459/// a violating terminal from `timing_path_inputs()`/`_outputs()` — but it
1460/// does so ONE TERMINAL AT A TIME, in `bindTerminal`, before this crate
1461/// ever sees the symbol: `path.timing_path_inputs()`/`_outputs()` (what
1462/// `discover_path` iterates to build `raw_inputs`/`raw_outputs`, and the
1463/// only source this function's own `sym` argument ever comes from) has
1464/// *already* had every terminal failing this exact check removed. So this
1465/// function's own checks below are, today, unreachable dead code against
1466/// sv-lang 0.1.9 — verified live, not a theoretical claim: every `sym` this
1467/// function is ever called with has already passed
1468/// `checkPathTerminal`'s identical net-kind/port-direction test. It is kept
1469/// anyway as defense-in-depth against a future sv-lang version relaxing
1470/// that filter, or an AST shape this module's own enumeration above has not
1471/// considered — never as this module's real guarantee against a dropped
1472/// sibling terminal in an otherwise-valid multi-terminal declaration. THAT
1473/// guarantee — a multi-terminal `path_declaration` with one invalid
1474/// sibling among otherwise-valid terminals is a `LoweringError`, never a
1475/// silently-accepted partial `PathEntry` — is [`discover_path`]'s own
1476/// syntax-vs-resolved terminal COUNT check (via
1477/// [`syntax_terminal_counts`]), not this function: this function only ever
1478/// sees the terminals sv-lang's per-terminal filtering already decided to
1479/// keep, so it cannot by construction "catch a violation regardless of how
1480/// many sibling terminals are otherwise valid" the way an earlier revision
1481/// of this comment claimed.
1482fn validate_direction<'d>(
1483    path: &Symbol<'d>,
1484    ports: &HashMap<SymbolId, ArgumentDirection>,
1485    sym: Symbol<'d>,
1486    want_input: bool,
1487) -> Result<(), ElabError> {
1488    if sym.kind() == SymbolKind::ModportPort {
1489        // LRM 25.6: every signal of an interface port is a terminal, with the
1490        // direction the modport gives it (an interface port without a modport
1491        // never reaches here: it names the interface's own signal). The
1492        // signal lives in the interface instance; the module's own signal
1493        // table carries it under the port's uid (run.rs foreign slots).
1494        // A `ref` port cannot be a terminal; an expression-bound port
1495        // (`.x(r[3:0])`) is not a plain signal.
1496        let internal = sym.modport_port_internal_symbol();
1497        if sym.modport_port_explicit_connection().is_some()
1498            || !internal.is_some_and(|s| matches!(s.kind(), SymbolKind::Net | SymbolKind::Variable))
1499        {
1500            return Err(unsupported(
1501                path,
1502                format!(
1503                    "specify terminal `{}` is an interface-port signal bound to an expression \
1504                     or a non-signal (LRM 25.6): not lowered",
1505                    sym.name()
1506                ),
1507            ));
1508        }
1509        let dir = sym.modport_port_direction();
1510        let ok = if want_input {
1511            matches!(dir, ArgumentDirection::In | ArgumentDirection::InOut)
1512        } else {
1513            matches!(dir, ArgumentDirection::Out | ArgumentDirection::InOut)
1514        };
1515        if !ok {
1516            return Err(unsupported(
1517                path,
1518                format!(
1519                    "specify path {} `{}` is a modport port with direction {:?}, not {} (LRM \
1520                     25.6, 30.4.1)",
1521                    if want_input { "source" } else { "destination" },
1522                    sym.name(),
1523                    dir,
1524                    if want_input {
1525                        "input or inout"
1526                    } else {
1527                        "output or inout"
1528                    }
1529                ),
1530            ));
1531        }
1532        return Ok(());
1533    }
1534    if want_input && sym.kind() != SymbolKind::Net {
1535        return Err(unsupported(
1536            path,
1537            format!(
1538                "specify path source `{}` is a {:?}, not a net (LRM 30.4.1: \"the module path \
1539                 source shall be a net\")",
1540                sym.name(),
1541                sym.kind()
1542            ),
1543        ));
1544    }
1545    if !want_input && !matches!(sym.kind(), SymbolKind::Net | SymbolKind::Variable) {
1546        return Err(unsupported(
1547            path,
1548            format!(
1549                "specify path destination `{}` is a {:?}, not a net or variable (LRM 30.4.1: \
1550                 \"the module path destination shall be a net or variable\")",
1551                sym.name(),
1552                sym.kind()
1553            ),
1554        ));
1555    }
1556    let dir = ports.get(&sym.uid()).ok_or_else(|| {
1557        unsupported(
1558            path,
1559            format!(
1560                "specify path terminal `{}` is not connected to any module port (LRM 30.4.1 \
1561                 requires the source to be connected to an input/inout port, and the \
1562                 destination to an output/inout port)",
1563                sym.name()
1564            ),
1565        )
1566    })?;
1567    let ok = if want_input {
1568        matches!(dir, ArgumentDirection::In | ArgumentDirection::InOut)
1569    } else {
1570        matches!(dir, ArgumentDirection::Out | ArgumentDirection::InOut)
1571    };
1572    if !ok {
1573        return Err(unsupported(
1574            path,
1575            format!(
1576                "specify path {} `{}` is connected to a {:?} port, not {} (LRM 30.4.1)",
1577                if want_input { "source" } else { "destination" },
1578                sym.name(),
1579                dir,
1580                if want_input {
1581                    "an input or inout port"
1582                } else {
1583                    "an output or inout port"
1584                }
1585            ),
1586        ));
1587    }
1588    Ok(())
1589}
1590
1591/// Resolves one `timing_path_inputs()`/`timing_path_outputs()` item to the
1592/// `TerminalRef` a [`PathEntry`] stores, validating [`validate_direction`]
1593/// along the way. Handles a plain signal reference, a constant bit-select
1594/// (`a[3]`) and a constant SIMPLE part-select (`a[7:4]`) — mirrors
1595/// `sv-sim-bin::primitives::signal_bit`'s own `Conversion`-unwrap/
1596/// `ElementSelect` handling, generalized with a part-select case
1597/// `primitives.rs`'s own scalar-terminal callers never needed.
1598///
1599/// An INDEXED part-select (`a[base +: w]`/`a[base -: w]`) with constant
1600/// operands resolves to the same bit window as the equivalent simple
1601/// part-select (LRM 11.5.1; bounds zero-based like the simple form).
1602fn resolve_terminal<'d>(
1603    eval: &EvalSession<'d>,
1604    path: &Symbol<'d>,
1605    ports: &HashMap<SymbolId, ArgumentDirection>,
1606    e: Expression<'d>,
1607    want_input: bool,
1608) -> Result<TerminalRef, ElabError> {
1609    let e = if e.kind() == ExpressionKind::Conversion {
1610        e.operand().unwrap_or(e)
1611    } else {
1612        e
1613    };
1614    let (sym, bits) = if is_value_name(e.kind()) {
1615        let sym = e.referenced_symbol().ok_or_else(|| {
1616            unsupported(
1617                path,
1618                "specify path terminal has no referenced symbol (unexpected AST shape)".to_string(),
1619            )
1620        })?;
1621        (sym, None)
1622    } else if e.kind() == ExpressionKind::ElementSelect {
1623        let base = e.select_value().ok_or_else(|| {
1624            unsupported(
1625                path,
1626                "bit-select specify path terminal missing its own base expression".to_string(),
1627            )
1628        })?;
1629        if !is_value_name(base.kind()) {
1630            return Err(unsupported(
1631                path,
1632                "bit-select specify path terminal's base is not a plain signal reference \
1633                 (unexpected AST shape)"
1634                    .to_string(),
1635            ));
1636        }
1637        let sym = base.referenced_symbol().ok_or_else(|| {
1638            unsupported(
1639                path,
1640                "bit-select specify path terminal's base has no referenced symbol (unexpected \
1641                 AST shape)"
1642                    .to_string(),
1643            )
1644        })?;
1645        let idx_expr = e.selector().ok_or_else(|| {
1646            unsupported(
1647                path,
1648                "bit-select specify path terminal missing its own index".to_string(),
1649            )
1650        })?;
1651        let idx = const_i64(eval, idx_expr).ok_or_else(|| {
1652            unsupported(
1653                path,
1654                "bit-select specify path terminal index must be a compile-time constant"
1655                    .to_string(),
1656            )
1657        })?;
1658        if idx < 0 {
1659            return Err(unsupported(
1660                path,
1661                "bit-select specify path terminal index is negative".to_string(),
1662            ));
1663        }
1664        (sym, Some((idx as u32, 1)))
1665    } else if e.kind() == ExpressionKind::RangeSelect {
1666        let base = e.select_value().ok_or_else(|| {
1667            unsupported(
1668                path,
1669                "part-select specify path terminal missing its own base expression".to_string(),
1670            )
1671        })?;
1672        if !is_value_name(base.kind()) {
1673            return Err(unsupported(
1674                path,
1675                "part-select specify path terminal's base is not a plain signal reference \
1676                 (unexpected AST shape)"
1677                    .to_string(),
1678            ));
1679        }
1680        let sym = base.referenced_symbol().ok_or_else(|| {
1681            unsupported(
1682                path,
1683                "part-select specify path terminal's base has no referenced symbol (unexpected \
1684                 AST shape)"
1685                    .to_string(),
1686            )
1687        })?;
1688        let kind = e.range_selection_kind().ok_or_else(|| {
1689            unsupported(
1690                path,
1691                "part-select specify path terminal missing its own range_selection_kind"
1692                    .to_string(),
1693            )
1694        })?;
1695        let l_expr = e.range_left().ok_or_else(|| {
1696            unsupported(
1697                path,
1698                "simple part-select specify path terminal missing its left bound".to_string(),
1699            )
1700        })?;
1701        let r_expr = e.range_right().ok_or_else(|| {
1702            unsupported(
1703                path,
1704                "simple part-select specify path terminal missing its right bound".to_string(),
1705            )
1706        })?;
1707        let l = const_i64(eval, l_expr).ok_or_else(|| {
1708            unsupported(
1709                path,
1710                "simple part-select specify path terminal's left bound must be a compile-time \
1711                 constant"
1712                    .to_string(),
1713            )
1714        })?;
1715        let r = const_i64(eval, r_expr).ok_or_else(|| {
1716            unsupported(
1717                path,
1718                "simple part-select specify path terminal's right bound must be a \
1719                 compile-time constant"
1720                    .to_string(),
1721            )
1722        })?;
1723        // LRM 11.5.1 / 30.3: `[l +: r]` is bits `l .. l+r-1`, `[l -: r]` is
1724        // `l-r+1 .. l` (kind 1 = up, 2 = down; 0 = simple `[l:r]`). Like the
1725        // simple form the bounds are taken as zero-based offsets.
1726        let (lo, hi) = match kind {
1727            0 => (l.min(r), l.max(r)),
1728            1 => (l, l + r - 1),
1729            _ => (l - r + 1, l),
1730        };
1731        if lo < 0 || hi < lo {
1732            return Err(unsupported(
1733                path,
1734                "part-select specify path terminal has a negative or empty bit window".to_string(),
1735            ));
1736        }
1737        (sym, Some((lo as u32, (hi - lo + 1) as u32)))
1738    } else {
1739        return Err(unsupported(
1740            path,
1741            format!(
1742                "specify path terminal expression kind `{:?}` (unexpected AST shape)",
1743                e.kind()
1744            ),
1745        ));
1746    };
1747    validate_direction(path, ports, sym, want_input)?;
1748    Ok((sym.uid(), bits))
1749}
1750
1751/// `b`'s value as [`Bit4`] -- the one place `sv_lang::Bit`'s discriminant
1752/// order is asserted to equal [`Bit4`]'s in THIS crate's `netlist` module
1753/// (mirrors `state::value::bit_to_bit4`'s own identical, independently-
1754/// written copy; a third small duplicate for the same reason `sv-sim-bin::
1755/// specify_eval`'s own `constant_of` doc comment gives for its own
1756/// duplicate of `primitives::constant_of` -- `state::value`'s copy is a
1757/// private `fn` of a sibling module this one cannot reach into).
1758fn bit4_of(b: Bit) -> Bit4 {
1759    match b {
1760        Bit::Zero => Bit4::Zero,
1761        Bit::One => Bit4::One,
1762        Bit::X => Bit4::X,
1763        Bit::Z => Bit4::Z,
1764    }
1765}
1766
1767/// Resolves a condition operand that is a plain signal/specparam reference,
1768/// a constant bit-select, or a constant SIMPLE part-select -- the
1769/// condition-expression analogue of [`resolve_terminal`] (same AST shapes,
1770/// same `ElementSelect`/`RangeSelect` handling), but WITHOUT
1771/// [`validate_direction`]'s port-direction check: LRM 30.4.4.1's own
1772/// operand list ("module input/inout ports... OR locally defined variables
1773/// or nets") is strictly WIDER than a module-path terminal's own 30.4.1
1774/// restriction (a plain internal variable/net, not connected to any port at
1775/// all, is a legal condition operand but never a legal path terminal), so
1776/// this function resolves the symbol and its bit window only, leaving any
1777/// further legality check to the caller (today: none -- see
1778/// [`compile_cond_expr`]'s own doc comment for why this task does not add
1779/// one).
1780fn resolve_cond_signal_ref<'d>(
1781    eval: &EvalSession<'d>,
1782    path: &Symbol<'d>,
1783    e: Expression<'d>,
1784) -> Result<(Symbol<'d>, Option<(u32, u32)>), ElabError> {
1785    if is_value_name(e.kind()) {
1786        let sym = e.referenced_symbol().ok_or_else(|| {
1787            unsupported(
1788                path,
1789                "state-dependent path condition operand has no referenced symbol (unexpected \
1790                 AST shape)"
1791                    .to_string(),
1792            )
1793        })?;
1794        return Ok((sym, None));
1795    }
1796    if e.kind() == ExpressionKind::ElementSelect {
1797        let base = e.select_value().ok_or_else(|| {
1798            unsupported(
1799                path,
1800                "bit-select condition operand missing its own base expression".to_string(),
1801            )
1802        })?;
1803        if !is_value_name(base.kind()) {
1804            return Err(unsupported(
1805                path,
1806                "bit-select condition operand's base is not a plain signal reference \
1807                 (unexpected AST shape)"
1808                    .to_string(),
1809            ));
1810        }
1811        let sym = base.referenced_symbol().ok_or_else(|| {
1812            unsupported(
1813                path,
1814                "bit-select condition operand's base has no referenced symbol (unexpected AST \
1815                 shape)"
1816                    .to_string(),
1817            )
1818        })?;
1819        let idx_expr = e.selector().ok_or_else(|| {
1820            unsupported(
1821                path,
1822                "bit-select condition operand missing its own index".to_string(),
1823            )
1824        })?;
1825        let idx = const_i64(eval, idx_expr).ok_or_else(|| {
1826            unsupported(
1827                path,
1828                "bit-select condition operand index must be a compile-time constant".to_string(),
1829            )
1830        })?;
1831        if idx < 0 {
1832            return Err(unsupported(
1833                path,
1834                "bit-select condition operand index is negative".to_string(),
1835            ));
1836        }
1837        return Ok((sym, Some((idx as u32, 1))));
1838    }
1839    if e.kind() == ExpressionKind::RangeSelect {
1840        let base = e.select_value().ok_or_else(|| {
1841            unsupported(
1842                path,
1843                "part-select condition operand missing its own base expression".to_string(),
1844            )
1845        })?;
1846        if !is_value_name(base.kind()) {
1847            return Err(unsupported(
1848                path,
1849                "part-select condition operand's base is not a plain signal reference \
1850                 (unexpected AST shape)"
1851                    .to_string(),
1852            ));
1853        }
1854        let sym = base.referenced_symbol().ok_or_else(|| {
1855            unsupported(
1856                path,
1857                "part-select condition operand's base has no referenced symbol (unexpected AST \
1858                 shape)"
1859                    .to_string(),
1860            )
1861        })?;
1862        let kind = e.range_selection_kind().ok_or_else(|| {
1863            unsupported(
1864                path,
1865                "part-select condition operand missing its own range_selection_kind".to_string(),
1866            )
1867        })?;
1868        let l_expr = e.range_left().ok_or_else(|| {
1869            unsupported(
1870                path,
1871                "simple part-select condition operand missing its left bound".to_string(),
1872            )
1873        })?;
1874        let r_expr = e.range_right().ok_or_else(|| {
1875            unsupported(
1876                path,
1877                "simple part-select condition operand missing its right bound".to_string(),
1878            )
1879        })?;
1880        let l = const_i64(eval, l_expr).ok_or_else(|| {
1881            unsupported(
1882                path,
1883                "simple part-select condition operand's left bound must be a compile-time \
1884                 constant"
1885                    .to_string(),
1886            )
1887        })?;
1888        let r = const_i64(eval, r_expr).ok_or_else(|| {
1889            unsupported(
1890                path,
1891                "simple part-select condition operand's right bound must be a compile-time \
1892                 constant"
1893                    .to_string(),
1894            )
1895        })?;
1896        // LRM 11.5.1 / 30.4.4.1: an indexed part-select with constant
1897        // operands. `l` is the base, `r` the width: `[l +: r]` is bits
1898        // `l .. l+r-1`, `[l -: r]` is `l-r+1 .. l` (kind 1 = up, 2 = down;
1899        // 0 = simple `[l:r]`). Like the simple form this takes the bounds as
1900        // zero-based offsets (a `[N:0]` declaration).
1901        let (lo, hi) = match kind {
1902            0 => (l.min(r), l.max(r)),
1903            1 => (l, l + r - 1),
1904            _ => (l - r + 1, l),
1905        };
1906        if lo < 0 || hi < lo {
1907            return Err(unsupported(
1908                path,
1909                "part-select condition operand has a negative or empty bit window".to_string(),
1910            ));
1911        }
1912        return Ok((sym, Some((lo as u32, (hi - lo + 1) as u32))));
1913    }
1914    Err(unsupported(
1915        path,
1916        format!(
1917            "state-dependent path condition operand expression kind `{:?}` is neither a \
1918             signal/specparam reference nor a bit-/part-select of one (unexpected AST shape)",
1919            e.kind()
1920        ),
1921    ))
1922}
1923
1924/// Compiles a state-dependent path's own `if (cond)` conditional expression
1925/// (LRM 30.4.4.1) into a [`CondNode`] -- see that type's own doc comment
1926/// for why this produces symbolic data rather than a value. Table 30-1's
1927/// own closed operator list is enforced via [`CondUnaryOp::from_sv`]/
1928/// [`CondBinaryOp::from_sv`]; any operator, or any operand AST shape, Table
1929/// 30-1/30.4.4.1 do not cover is a named [`ElabError::UnsupportedConstruct`],
1930/// never a silent acceptance.
1931///
1932/// This task does not re-enforce 30.4.4.1's own operand-KIND restriction
1933/// (ports/variables/nets only, never e.g. a class property or a function
1934/// call) beyond what [`resolve_cond_signal_ref`]'s own AST-shape dispatch
1935/// already excludes by construction (a function call, for instance, is
1936/// none of `NamedValue`/`ElementSelect`/`RangeSelect`/a foldable constant,
1937/// so it already falls through to this function's own trailing `Err`) --
1938/// mirrors [`validate_direction`]'s own documented reasoning that slang's
1939/// own elaboration-time checks make a second, redundant kind check
1940/// pure defense-in-depth rather than this function's real guarantee.
1941fn compile_cond_expr<'d>(
1942    eval: &EvalSession<'d>,
1943    path: &Symbol<'d>,
1944    e: Expression<'d>,
1945) -> Result<CondNode, ElabError> {
1946    compile_cond_expr_in(eval, path, e, CondDialect::ModulePath)
1947}
1948
1949/// Which clause's operator set a condition expression is compiled under.
1950#[derive(Clone, Copy, PartialEq, Eq)]
1951enum CondDialect {
1952    /// A state-dependent module path's `if (cond)` (LRM 30.4.4.1, Table 30-1).
1953    ModulePath,
1954    /// A system timing check's `&&&`/timestamp/timecheck condition (LRM 31.7):
1955    /// Table 30-1 plus `===`/`!==`.
1956    TimingCheck,
1957}
1958
1959fn compile_cond_expr_in<'d>(
1960    eval: &EvalSession<'d>,
1961    path: &Symbol<'d>,
1962    e: Expression<'d>,
1963    dialect: CondDialect,
1964) -> Result<CondNode, ElabError> {
1965    let e = if e.kind() == ExpressionKind::Conversion {
1966        e.operand().unwrap_or(e)
1967    } else {
1968        e
1969    };
1970    match e.kind() {
1971        ExpressionKind::UnaryOp => {
1972            let uop = e.unary_op().ok_or_else(|| {
1973                unsupported(
1974                    path,
1975                    "state-dependent path condition's unary expression is missing its own \
1976                     operator (unexpected AST shape)"
1977                        .to_string(),
1978                )
1979            })?;
1980            let cop = CondUnaryOp::from_sv(uop).ok_or_else(|| {
1981                unsupported(
1982                    path,
1983                    format!(
1984                        "state-dependent path condition uses unary operator `{uop:?}`, which is \
1985                         not one of LRM Table 30-1's own eight valid conditional-expression \
1986                         operators"
1987                    ),
1988                )
1989            })?;
1990            let operand = e.operand().ok_or_else(|| {
1991                unsupported(
1992                    path,
1993                    "state-dependent path condition's unary expression is missing its own \
1994                     operand (unexpected AST shape)"
1995                        .to_string(),
1996                )
1997            })?;
1998            Ok(CondNode::Unary(
1999                cop,
2000                Box::new(compile_cond_expr_in(eval, path, operand, dialect)?),
2001            ))
2002        }
2003        ExpressionKind::BinaryOp => {
2004            let bop = e.binary_op().ok_or_else(|| {
2005                unsupported(
2006                    path,
2007                    "state-dependent path condition's binary expression is missing its own \
2008                     operator (unexpected AST shape)"
2009                        .to_string(),
2010                )
2011            })?;
2012            let mapped = match dialect {
2013                CondDialect::ModulePath => CondBinaryOp::from_sv(bop),
2014                CondDialect::TimingCheck => CondBinaryOp::from_sv_check(bop),
2015            };
2016            let cop = mapped.ok_or_else(|| {
2017                unsupported(
2018                    path,
2019                    format!(
2020                        "state-dependent path condition uses binary operator `{bop:?}`, which \
2021                         is not one of LRM Table 30-1's own eight valid conditional-expression \
2022                         operators"
2023                    ),
2024                )
2025            })?;
2026            let l = e.left().ok_or_else(|| {
2027                unsupported(
2028                    path,
2029                    "state-dependent path condition's binary expression is missing its own \
2030                     left operand (unexpected AST shape)"
2031                        .to_string(),
2032                )
2033            })?;
2034            let r = e.right().ok_or_else(|| {
2035                unsupported(
2036                    path,
2037                    "state-dependent path condition's binary expression is missing its own \
2038                     right operand (unexpected AST shape)"
2039                        .to_string(),
2040                )
2041            })?;
2042            Ok(CondNode::Binary(
2043                cop,
2044                Box::new(compile_cond_expr_in(eval, path, l, dialect)?),
2045                Box::new(compile_cond_expr_in(eval, path, r, dialect)?),
2046            ))
2047        }
2048        ExpressionKind::ConditionalOp => {
2049            let conds = e.conditions();
2050            if conds.len() != 1 || conds[0].pattern().is_some() {
2051                return Err(unsupported(
2052                    path,
2053                    "state-dependent path condition uses a pattern-matching conditional \
2054                     operator (`&&&`-chained or `matches`), which LRM Table 30-1 does not list"
2055                        .to_string(),
2056                ));
2057            }
2058            let c = compile_cond_expr_in(eval, path, conds[0].expr(), dialect)?;
2059            let t = e.true_value().ok_or_else(|| {
2060                unsupported(
2061                    path,
2062                    "state-dependent path condition's conditional operator is missing its own \
2063                     true-value operand (unexpected AST shape)"
2064                        .to_string(),
2065                )
2066            })?;
2067            let f = e.false_value().ok_or_else(|| {
2068                unsupported(
2069                    path,
2070                    "state-dependent path condition's conditional operator is missing its own \
2071                     false-value operand (unexpected AST shape)"
2072                        .to_string(),
2073                )
2074            })?;
2075            Ok(CondNode::Ternary(
2076                Box::new(c),
2077                Box::new(compile_cond_expr_in(eval, path, t, dialect)?),
2078                Box::new(compile_cond_expr_in(eval, path, f, dialect)?),
2079            ))
2080        }
2081        ExpressionKind::Concatenation => {
2082            let operands = e.operands();
2083            if operands.is_empty() {
2084                return Err(unsupported(
2085                    path,
2086                    "state-dependent path condition's concatenation has no operands \
2087                     (unexpected AST shape)"
2088                        .to_string(),
2089                ));
2090            }
2091            let mut out = Vec::with_capacity(operands.len());
2092            for o in operands {
2093                out.push(compile_cond_expr_in(eval, path, o, dialect)?);
2094            }
2095            Ok(CondNode::Concat(out))
2096        }
2097        ExpressionKind::Replication => {
2098            let count_expr = e.replication_count().ok_or_else(|| {
2099                unsupported(
2100                    path,
2101                    "state-dependent path condition's replication is missing its own count \
2102                     (unexpected AST shape)"
2103                        .to_string(),
2104                )
2105            })?;
2106            let count = const_i64(eval, count_expr).ok_or_else(|| {
2107                unsupported(
2108                    path,
2109                    "state-dependent path condition's replication count must be a compile-time \
2110                     constant"
2111                        .to_string(),
2112                )
2113            })?;
2114            if count < 0 {
2115                return Err(unsupported(
2116                    path,
2117                    "state-dependent path condition's replication count is negative".to_string(),
2118                ));
2119            }
2120            let unit = e.replication_concat().ok_or_else(|| {
2121                unsupported(
2122                    path,
2123                    "state-dependent path condition's replication is missing its own repeated \
2124                     operand (unexpected AST shape)"
2125                        .to_string(),
2126                )
2127            })?;
2128            Ok(CondNode::Replication(
2129                count as u32,
2130                Box::new(compile_cond_expr_in(eval, path, unit, dialect)?),
2131            ))
2132        }
2133        ExpressionKind::NamedValue
2134        | ExpressionKind::HierarchicalValue
2135        | ExpressionKind::ElementSelect
2136        | ExpressionKind::RangeSelect => {
2137            let (sym, bits) = resolve_cond_signal_ref(eval, path, e)?;
2138            if sym.kind() == SymbolKind::Specparam {
2139                Ok(CondNode::Specparam(sym.uid()))
2140            } else {
2141                Ok(CondNode::Signal((sym.uid(), bits)))
2142            }
2143        }
2144        // LRM 30.4.4.1's own third operand kind, "compile-time constants
2145        // (constant numbers...)" -- a plain NUMBER LITERAL, the only shape
2146        // this clause's own text means by it (an arithmetic/relational
2147        // constant EXPRESSION built from these, e.g. `(2+3)`, is already
2148        // illegal regardless of foldability, since `+`/`<` etc. are not
2149        // among Table 30-1's own eight operators -- rejected by the
2150        // `UnaryOp`/`BinaryOp` arms above via `CondUnaryOp`/`CondBinaryOp::
2151        // from_sv`, never reaching here). Named explicitly, one arm per
2152        // literal `ExpressionKind` (not a catch-all): `cargo xtask
2153        // lint-unimplemented`'s own static walk singles out a WILDCARD arm
2154        // for this match for extra scrutiny (it must be unconditionally
2155        // loud, since the lint cannot otherwise tell which kinds a
2156        // "try to fold it, fail loudly if that fails too" catch-all
2157        // silently accepts) -- enumerating the literal kinds here keeps the
2158        // trailing `_` arm honestly, unconditionally loud instead.
2159        ExpressionKind::IntegerLiteral | ExpressionKind::UnbasedUnsizedIntegerLiteral => {
2160            let c = match e.constant_value().or_else(|| eval.eval_constant(e)) {
2161                Some(c) => c,
2162                None => {
2163                    return Err(unsupported(
2164                        path,
2165                        "state-dependent path condition's integer literal operand did not fold \
2166                         to a constant (unexpected AST shape)"
2167                            .to_string(),
2168                    ));
2169                }
2170            };
2171            let int = match c.as_integer() {
2172                Some(int) => int,
2173                None => {
2174                    return Err(unsupported(
2175                        path,
2176                        "state-dependent path condition's integer literal operand did not fold \
2177                         to an integer constant (unexpected AST shape)"
2178                            .to_string(),
2179                    ));
2180                }
2181            };
2182            let bits = int.bits();
2183            if bits.is_empty() && int.bit_width() > 0 {
2184                return Err(unsupported(
2185                    path,
2186                    "state-dependent path condition's integer literal operand is too wide to \
2187                     expand bit-by-bit"
2188                        .to_string(),
2189                ));
2190            }
2191            Ok(CondNode::Const(bits.iter().map(|&b| bit4_of(b)).collect()))
2192        }
2193        ExpressionKind::RealLiteral
2194        | ExpressionKind::TimeLiteral
2195        | ExpressionKind::NullLiteral
2196        | ExpressionKind::UnboundedLiteral
2197        | ExpressionKind::StringLiteral => Err(unsupported(
2198            path,
2199            format!(
2200                "state-dependent path condition operand of kind `{:?}` is not an integer \
2201                 constant number -- LRM 30.4.4.1's own \"compile-time constants\" operand kind \
2202                 means a constant NUMBER, never a real/time/null/unbounded/string literal",
2203                e.kind()
2204            ),
2205        )),
2206        _ => Err(unsupported(
2207            path,
2208            format!(
2209                "state-dependent path condition operand expression kind `{:?}` is none of LRM \
2210                 30.4.4.1's own operand kinds (a module input/inout port, a locally defined \
2211                 variable or net, their bit-/part-selects, or a compile-time constant number) \
2212                 and is not one of the operator shapes this function otherwise handles \
2213                 (unexpected AST shape)",
2214                e.kind()
2215            ),
2216        )),
2217    }
2218}
2219
2220/// `TimingPath::timing_path_is_state_dependent()`/`_condition_expr()`'s own
2221/// joint discriminant (this plan's own "What already exists" section's
2222/// decision 0): neither accessor alone distinguishes all three of
2223/// [`PathCondition`]'s own cases -- `is_state_dependent() == false` is
2224/// always [`PathCondition::Unconditional`]; `is_state_dependent() == true`
2225/// with `condition_expr().is_some()` is [`PathCondition::If`] (the
2226/// expression compiled via [`compile_cond_expr`]); `is_state_dependent() ==
2227/// true` with `condition_expr().is_none()` is [`PathCondition::IfNone`] (an
2228/// `ifnone` path has no condition expression of its own at all).
2229fn resolve_condition<'d>(
2230    eval: &EvalSession<'d>,
2231    path: &Symbol<'d>,
2232) -> Result<PathCondition, ElabError> {
2233    if !path.timing_path_is_state_dependent() {
2234        return Ok(PathCondition::Unconditional);
2235    }
2236    match path.timing_path_condition_expr() {
2237        Some(cond) => Ok(PathCondition::If(compile_cond_expr(eval, path, cond)?)),
2238        None => Ok(PathCondition::IfNone),
2239    }
2240}
2241
2242/// A module-path terminal's own "reference shape" -- LRM 30.4.4.3's own
2243/// "the port is referenced in the same way in all path declarations
2244/// (entire port, bit-select, or part-select)" -- the SHAPE category only
2245/// (never the specific indices: Example 4's own `q[3:0]` vs `q[0]` is
2246/// illegal because one is a part-select and the other a bit-select, not
2247/// because their indices differ).
2248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2249enum TerminalShape {
2250    Whole,
2251    BitSelect,
2252    PartSelect,
2253}
2254
2255impl TerminalShape {
2256    fn of(bits: &Option<(u32, u32)>) -> TerminalShape {
2257        match bits {
2258            None => TerminalShape::Whole,
2259            Some((_, 1)) => TerminalShape::BitSelect,
2260            Some((_, _)) => TerminalShape::PartSelect,
2261        }
2262    }
2263}
2264
2265/// LRM 30.4.4.3's "same port reference shape" requirement and 30.4.4.4's
2266/// "ifnone + unconditional for the same path is illegal" rule -- Task 8's
2267/// own two discovery-time validations (its own text: "both `LoweringError`s,
2268/// not silent acceptance"), run once `instance`'s every desugared
2269/// [`PathEntry`] has been discovered. Both group entries by `(input
2270/// terminal symbol, output terminal symbol)` -- "the same module path" per
2271/// 30.4.4.4's own text, and what this plan's own T8 section calls "what
2272/// full-path grouping identifies as 'the same path'" for 30.4.4.3.
2273///
2274/// Scope note (task's own documented choice): 30.4.4.3's subclause title is
2275/// "Edge-sensitive state-dependent paths," but its own criteria are framed
2276/// generically over "the same path," and the plan's own T8 text does not
2277/// limit the shape check to edge-sensitive declarations specifically -- so
2278/// this validates the destination's own shape agreement across every
2279/// state-dependent (`If`/`IfNone`) entry of a group, edge-sensitive or not.
2280/// Only the DESTINATION's shape is compared (30.4.4.4's own Example 4 only
2281/// ever illustrates a destination mismatch); a source-shape mismatch is not
2282/// validated here.
2283fn validate_state_dependent_groups(
2284    instance: InstanceId,
2285    paths: &[PathEntry],
2286) -> Result<(), ElabError> {
2287    let mut groups: HashMap<(SymbolId, SymbolId), Vec<&PathEntry>> = HashMap::new();
2288    for p in paths.iter().filter(|p| p.instance == instance) {
2289        let key = (p.inputs[0].0.clone(), p.outputs[0].0.clone());
2290        groups.entry(key).or_default().push(p);
2291    }
2292    for ((in_uid, out_uid), group) in &groups {
2293        let has_ifnone = group
2294            .iter()
2295            .any(|p| matches!(p.condition, PathCondition::IfNone));
2296        let has_unconditional = group
2297            .iter()
2298            .any(|p| matches!(p.condition, PathCondition::Unconditional));
2299        if has_ifnone && has_unconditional {
2300            return Err(ElabError::UnsupportedConstruct {
2301                hier_path: format!("{in_uid} -> {out_uid}"),
2302                detail: "an `ifnone` condition and an unconditional module path both declared \
2303                         for the same module path source/destination pair (LRM 30.4.4.4: \"It \
2304                         is illegal to specify both an ifnone condition for a module path and \
2305                         an unconditional simple module path for the same module path.\")"
2306                    .to_string(),
2307            });
2308        }
2309        let mut shape: Option<TerminalShape> = None;
2310        for p in group
2311            .iter()
2312            .filter(|p| !matches!(p.condition, PathCondition::Unconditional))
2313        {
2314            let s = TerminalShape::of(&p.outputs[0].1);
2315            match shape {
2316                None => shape = Some(s),
2317                Some(prev) if prev == s => {}
2318                Some(_) => {
2319                    return Err(ElabError::UnsupportedConstruct {
2320                        hier_path: format!("{in_uid} -> {out_uid}"),
2321                        detail: "state-dependent module path declarations for the same path \
2322                                 reference their destination port in different ways (whole \
2323                                 port vs. bit-select vs. part-select) -- LRM 30.4.4.3: \"The \
2324                                 port is referenced in the same way in all path declarations \
2325                                 (entire port, bit-select, or part-select).\""
2326                            .to_string(),
2327                    });
2328                }
2329            }
2330        }
2331    }
2332    Ok(())
2333}
2334
2335/// Desugars one `TimingPath` symbol into its [`PathEntry`]s (LRM 30.4.6):
2336/// a `Full` (`*>`) connection with `n` inputs and `m` outputs becomes `n *
2337/// m` entries, one per (output, input) pair, in the SAME order the LRM's
2338/// own worked example lists them (outer loop over outputs, inner loop over
2339/// inputs — "(a*>q1);(b*>q1);(c*>q1);(a*>q2);(b*>q2);(c*>q2);" for
2340/// `(a,b,c*>q1,q2)`). A `Parallel` (`=>`) connection's own grammar
2341/// (`parallel_path_description`, LRM Annex A.7.2) only ever has a single
2342/// terminal on each side, so it is never desugared — a `Parallel`
2343/// `TimingPath` with more than one resolved input or output is an
2344/// unexpected AST shape, not a shape this function silently handles by
2345/// picking the first one.
2346///
2347/// A `TimingPath` whose `timing_path_inputs()`/`_outputs()` resolves to
2348/// ZERO terminals (LRM 30.4.1's restriction already rejected the only one
2349/// declared, slang's own diagnostic naming it — or, per
2350/// `resolve_terminal`'s own doc comment, an unsupported terminal form such
2351/// as `interface_identifier.port_identifier`: verified live, slang 11.0
2352/// rejects that form outright with "specify path must be a simple
2353/// reference to a module port signal") is a named error here, never a
2354/// silently-empty `Vec<PathEntry>` — the "never silently accepted" guarantee
2355/// this plan's own "What already exists" section commits to for Clause 30.
2356///
2357/// Review fix (r1): the zero-terminal checks above are not a COMPLETE
2358/// restriction check on their own. sv-lang's own per-terminal filtering
2359/// (`SpecifyBlockSymbol::checkPathTerminal`, consulted one
2360/// `specify_input_terminal_descriptor`/`specify_output_terminal_descriptor`
2361/// at a time) drops only the terminal(s) that actually violate 30.4.1,
2362/// keeping every sibling that does not — so a multi-terminal declaration
2363/// with one bad terminal among several good ones (e.g. `(a, r, c *> q1)`
2364/// where `r` is a `reg`) never hits the `is_empty()` guards above at all:
2365/// `raw_inputs`/`raw_outputs` end up non-empty, just SHORTER than what the
2366/// source actually wrote, and the bad sibling is silently dropped with zero
2367/// error — verified live (`crates/sv-sim-sim/tests/specify_discovery.rs`'s
2368/// `a_bad_sibling_input_terminal_in_an_otherwise_valid_multi_terminal_\
2369/// path_is_a_named_error`/`..._output_terminal_..._` tests). The count
2370/// check right after this function's own
2371/// `raw_inputs`/`raw_outputs` collection closes that gap directly, by
2372/// comparing against [`syntax_terminal_counts`]'s own syntax-level count
2373/// (from `list_of_path_inputs`/`list_of_path_outputs` itself, upstream of
2374/// any AST-level filtering) — independent of `validate_direction`, whose
2375/// own doc comment explains why it cannot do this.
2376fn discover_path<'d>(
2377    eval: &EvalSession<'d>,
2378    instance: InstanceId,
2379    path: &Symbol<'d>,
2380    ports: &HashMap<SymbolId, ArgumentDirection>,
2381    trees: &[SyntaxTree],
2382) -> Result<Vec<PathEntry>, ElabError> {
2383    let raw_inputs: Vec<_> = path.timing_path_inputs().collect();
2384    let raw_outputs: Vec<_> = path.timing_path_outputs().collect();
2385    if raw_inputs.is_empty() {
2386        return Err(unsupported(
2387            path,
2388            "specify path has no input terminal sv-lang could resolve (LRM 30.4.1's source \
2389             restriction already rejected it upstream, or it uses a terminal form this \
2390             workspace does not resolve, e.g. `interface_identifier.port_identifier`)"
2391                .to_string(),
2392        ));
2393    }
2394    if raw_outputs.is_empty() {
2395        return Err(unsupported(
2396            path,
2397            "specify path has no output terminal sv-lang could resolve (LRM 30.4.1's \
2398             destination restriction already rejected it upstream, or it uses a terminal form \
2399             this workspace does not resolve)"
2400                .to_string(),
2401        ));
2402    }
2403    // Non-empty is not the same as "nothing was dropped": compare against
2404    // the syntax-level terminal count (see `syntax_terminal_counts`'s own
2405    // doc comment) to catch a sibling terminal sv-lang's own per-terminal
2406    // filtering silently removed from an otherwise multi-terminal
2407    // declaration -- the gap the `is_empty()` guards above cannot see.
2408    let (syntax_input_count, syntax_output_count) = syntax_terminal_counts(path, trees)?;
2409    if raw_inputs.len() != syntax_input_count {
2410        return Err(unsupported(
2411            path,
2412            format!(
2413                "specify path's `list_of_path_inputs` names {syntax_input_count} terminal(s) in \
2414                 the source, but only {} resolved (LRM 30.4.1: \"the module path source shall be \
2415                 a net ... connected to an input or inout port\") -- at least one sibling input \
2416                 terminal violates this restriction and was silently dropped by sv-lang's own \
2417                 per-terminal filtering rather than failing the whole declaration",
2418                raw_inputs.len()
2419            ),
2420        ));
2421    }
2422    if raw_outputs.len() != syntax_output_count {
2423        return Err(unsupported(
2424            path,
2425            format!(
2426                "specify path's output terminal list names {syntax_output_count} terminal(s) in \
2427                 the source, but only {} resolved (LRM 30.4.1: \"the module path destination \
2428                 shall be a net or variable ... connected to an output or inout port\") -- at \
2429                 least one sibling output/destination terminal violates this restriction and \
2430                 was silently dropped by sv-lang's own per-terminal filtering rather than \
2431                 failing the whole declaration",
2432                raw_outputs.len()
2433            ),
2434        ));
2435    }
2436    let inputs = raw_inputs
2437        .into_iter()
2438        .map(|e| resolve_terminal(eval, path, ports, e, true))
2439        .collect::<Result<Vec<_>, _>>()?;
2440    let outputs = raw_outputs
2441        .into_iter()
2442        .map(|e| resolve_terminal(eval, path, ports, e, false))
2443        .collect::<Result<Vec<_>, _>>()?;
2444    let connection = path.timing_path_connection_kind();
2445    // LRM §30.4.7: the path operator's own overall polarity (`+*>`/`-*>`/
2446    // `+=>`/`-=>`, or none) -- metadata only, see [`PathEntry::polarity`]'s
2447    // own doc comment. Read once here: every desugared entry sharing this
2448    // one `path_declaration` (30.4.6) shares its one polarity too, exactly
2449    // like they already share one `path_uid`/delay list.
2450    let polarity = path.timing_path_polarity();
2451    let path_uid = path.uid();
2452    // LRM §30.4.3 (Task 7): this path's own edge-sensitivity, plus the
2453    // edge-sensitive suffix's data-source expression (reduced to a debug
2454    // name) -- the ONE call to `PathEdge::from_accessors` (hence to
2455    // `Symbol::timing_path_edge_source_expr()`) every desugared entry
2456    // sharing this one `path_declaration` reuses, exactly like
2457    // `polarity`/`path_uid`/the (not-yet-evaluated) delay list above.
2458    let (edge, edge_source) = PathEdge::from_accessors(path);
2459    let edge_data_source_name = edge_source
2460        .and_then(|e| e.referenced_symbol())
2461        .map(|s| s.name().to_string());
2462    // LRM §30.4.4 (Task 8): this path's own state-dependency, shared by
2463    // every desugared entry exactly like `polarity`/`edge`/`path_uid`
2464    // above (30.4.6: several entries sharing one declaration share its one
2465    // `if`/`ifnone` too).
2466    let condition = resolve_condition(eval, path)?;
2467    let mut out = Vec::new();
2468    match connection {
2469        sv_lang::TimingPathConnectionKind::Full => {
2470            for o in &outputs {
2471                for i in &inputs {
2472                    out.push(PathEntry {
2473                        instance,
2474                        inputs: vec![i.clone()],
2475                        outputs: vec![o.clone()],
2476                        connection,
2477                        path_uid: path_uid.clone(),
2478                        delays: PathDelaySet::default(),
2479                        polarity,
2480                        edge,
2481                        edge_data_source_name: edge_data_source_name.clone(),
2482                        condition: condition.clone(),
2483                        reject_limit: PathDelaySet::default(),
2484                        error_limit: PathDelaySet::default(),
2485                        pulse_style: PulseStyle::default(),
2486                        showcancelled: false,
2487                    });
2488                }
2489            }
2490        }
2491        sv_lang::TimingPathConnectionKind::Parallel => {
2492            if inputs.len() != 1 || outputs.len() != 1 {
2493                return Err(unsupported(
2494                    path,
2495                    format!(
2496                        "parallel (`=>`) module path with {} input(s) and {} output(s) \
2497                         (unexpected AST shape — LRM Annex A.7.2's `parallel_path_description` \
2498                         grammar only allows a single terminal on each side)",
2499                        inputs.len(),
2500                        outputs.len()
2501                    ),
2502                ));
2503            }
2504            out.push(PathEntry {
2505                instance,
2506                inputs,
2507                outputs,
2508                connection,
2509                path_uid,
2510                delays: PathDelaySet::default(),
2511                polarity,
2512                edge,
2513                edge_data_source_name,
2514                condition,
2515                reject_limit: PathDelaySet::default(),
2516                error_limit: PathDelaySet::default(),
2517                pulse_style: PulseStyle::default(),
2518                showcancelled: false,
2519            });
2520        }
2521    }
2522    Ok(out)
2523}
2524
2525/// `path`'s own `(input_count, output_count)` as actually WRITTEN in its
2526/// `path_declaration` syntax (Annex A.7.2's `list_of_path_inputs`/
2527/// `list_of_path_outputs`) — upstream of sv-lang's own per-terminal LRM
2528/// 30.4.1 filtering (`SpecifyBlockSymbol::checkPathTerminal`, consulted one
2529/// terminal at a time by `bindTerminal`), which is why `timing_path_
2530/// inputs()`/`_outputs()` (what `raw_inputs`/`raw_outputs` hold) cannot be
2531/// compared against each other to detect a dropped sibling: both sides of
2532/// that comparison would already be post-filtering. This function walks
2533/// `trees` for the one `path.syntax(tree)` resolves against — the SAME
2534/// `Symbol::syntax(&SyntaxTree)` pattern `sv-sim-bin::nets::net_kind_of`
2535/// already uses to recover a symbol's own declaration syntax — then
2536/// unwraps a `ConditionalPathDeclaration`/`IfNonePathDeclaration` wrapper
2537/// down to the underlying `PathDeclaration` the same way slang's own
2538/// `TimingPathSymbol::resolve()` does (`if (a) (b=>q)=1;` / `(b=>q)=1 \
2539/// else 1;` both still describe exactly one `list_of_path_inputs`/
2540/// `list_of_path_outputs` pair). `PathDescriptionSyntax::inputs()` is the
2541/// one input list shared by every path shape (simple AND edge-sensitive);
2542/// the output list lives on whichever `PathSuffixSyntax` variant the
2543/// declaration uses (`SimplePathSuffix` for `*>`/`=>`, `EdgeSensitivePathSuffix`
2544/// for `posedge clk => (q +: d)`-shaped paths), so both are matched
2545/// explicitly rather than assuming one shape.
2546fn syntax_terminal_counts<'d>(
2547    path: &Symbol<'d>,
2548    trees: &[SyntaxTree],
2549) -> Result<(usize, usize), ElabError> {
2550    let node = trees
2551        .iter()
2552        .find_map(|tree| path.syntax(tree))
2553        .ok_or_else(|| {
2554            unsupported(
2555                path,
2556                "specify path declaration has no syntax node in any of the design's syntax \
2557                 trees (unexpected AST shape)"
2558                    .to_string(),
2559            )
2560        })?;
2561    let decl = match node.kind() {
2562        SyntaxKind::PathDeclaration => <PathDeclarationSyntax as sv_lang::AstNode>::cast(node),
2563        SyntaxKind::ConditionalPathDeclaration => {
2564            <ConditionalPathDeclarationSyntax as sv_lang::AstNode>::cast(node).map(|n| n.path())
2565        }
2566        SyntaxKind::IfNonePathDeclaration => {
2567            <IfNonePathDeclarationSyntax as sv_lang::AstNode>::cast(node).map(|n| n.path())
2568        }
2569        other => {
2570            return Err(unsupported(
2571                path,
2572                format!(
2573                    "specify path declaration's own syntax node is of kind `{other:?}` \
2574                     (expected PathDeclaration, ConditionalPathDeclaration or \
2575                     IfNonePathDeclaration -- unexpected AST shape)"
2576                ),
2577            ));
2578        }
2579    }
2580    .ok_or_else(|| {
2581        unsupported(
2582            path,
2583            "specify path declaration's own syntax node failed to cast to its expected shape \
2584             (unexpected AST shape)"
2585                .to_string(),
2586        )
2587    })?;
2588    let desc = decl.desc();
2589    let input_count = desc.inputs().iter().count();
2590    let output_count = match desc.suffix() {
2591        PathSuffixSyntax::SimplePathSuffix(s) => s.outputs().iter().count(),
2592        PathSuffixSyntax::EdgeSensitivePathSuffix(s) => s.outputs().iter().count(),
2593    };
2594    Ok((input_count, output_count))
2595}
2596
2597#[cfg(test)]
2598mod tests {
2599    use sv_lang::{Compilation, ConstantValue, Design, Session, Symbol};
2600    use sv_lang_kinds::SymbolKind;
2601    use sv_sim_abi::bit4::Bit4;
2602
2603    use super::{PathDelaySet, PathEdge, PulseStyle, TimingModel};
2604    use crate::netlist::InstanceId;
2605    use crate::symid::SymbolUid;
2606
2607    fn compile(src: &str) -> Design {
2608        let session = Session::new();
2609        let mut comp = Compilation::new(&session).expect("compilation created");
2610        comp.add_source(src).expect("source added");
2611        comp.compile().expect("design compiled")
2612    }
2613
2614    fn top_body(design: &Design) -> Symbol<'_> {
2615        design
2616            .top_instances()
2617            .next()
2618            .expect("one top instance")
2619            .instance_body()
2620            .expect("top instance has a body")
2621    }
2622
2623    /// SDF's Table 32-4 expansion builds a table twelve values at a time; the array form
2624    /// round-trips in [`TRANSITIONS`] order, which is the order `of` indexes by.
2625    #[test]
2626    fn transitions_array_round_trips_in_the_documented_order() {
2627        let t = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21];
2628        let set = PathDelaySet::from_transitions(t);
2629        assert_eq!(set.transitions(), t);
2630        for (i, &(from, to)) in super::TRANSITIONS.iter().enumerate() {
2631            assert_eq!(set.of(from, to), t[i], "{from:?}->{to:?}");
2632        }
2633        // The 12-value `from_list` form copies in the same order.
2634        assert_eq!(PathDelaySet::from_list(&t).unwrap(), set);
2635    }
2636
2637    /// LRM 31.9.1: a path floor raises the transitions below it and leaves the rest alone,
2638    /// transition by transition (a 6-value list keeps its larger delays).
2639    #[test]
2640    fn at_least_raises_only_the_delays_below_the_floor() {
2641        let set = PathDelaySet::from_list(&[2, 9, 4, 3, 12, 1]).expect("six values");
2642        let raised = set.at_least(5);
2643        for (from, to) in [
2644            (Bit4::Zero, Bit4::One),
2645            (Bit4::One, Bit4::Zero),
2646            (Bit4::Zero, Bit4::Z),
2647            (Bit4::Z, Bit4::One),
2648            (Bit4::One, Bit4::Z),
2649            (Bit4::Z, Bit4::Zero),
2650            (Bit4::X, Bit4::One),
2651            (Bit4::One, Bit4::X),
2652        ] {
2653            assert_eq!(
2654                raised.of(from, to),
2655                set.of(from, to).max(5),
2656                "{from:?}->{to:?}"
2657            );
2658        }
2659        assert_eq!(raised.of(Bit4::One, Bit4::Zero), 9);
2660        assert_eq!(raised.of(Bit4::Zero, Bit4::One), 5);
2661        assert_eq!(raised.min_all(), 5);
2662        // A floor of 0 changes nothing.
2663        assert_eq!(set.at_least(0), set);
2664    }
2665
2666    #[test]
2667    fn seed_specparams_round_trips_constant_value() {
2668        let design =
2669            compile("module m;\n  specparam tRise = 10;\n  specparam tFall = 7;\nendmodule\n");
2670        let body = top_body(&design);
2671        let mut model = TimingModel::new();
2672        model.seed_specparams(&[(InstanceId(0), body)]);
2673
2674        let rise_uid = body
2675            .members()
2676            .find(|s| s.name() == "tRise")
2677            .expect("tRise member")
2678            .uid();
2679        let fall_uid = body
2680            .members()
2681            .find(|s| s.name() == "tFall")
2682            .expect("tFall member")
2683            .uid();
2684
2685        assert_eq!(
2686            model
2687                .specparam(InstanceId(0), &rise_uid)
2688                .and_then(ConstantValue::as_i64),
2689            Some(10)
2690        );
2691        assert_eq!(
2692            model
2693                .specparam(InstanceId(0), &fall_uid)
2694                .and_then(ConstantValue::as_i64),
2695            Some(7)
2696        );
2697        // No specparam was seeded under a different instance id.
2698        assert!(model.specparam(InstanceId(1), &rise_uid).is_none());
2699    }
2700
2701    #[test]
2702    fn set_specparam_overwrites_the_seeded_value() {
2703        // Simulates what Task 28's real `$sdf_annotate` LABEL annotation
2704        // will do: mutate an already-seeded table in place, through an API
2705        // that needs no live `EvalSession`/`Design` of its own.
2706        // `tFall`'s own frozen value stands in for the new, SDF-supplied
2707        // value Task 28 would compute from a real `.sdf` file -- this task
2708        // only needs SOME concrete, non-seeded `ConstantValue` to move the
2709        // table to, not a hand-built one (`sv_lang::SVInt` has no public
2710        // "build an owned integer of value N" constructor outside the
2711        // frozen-design read path this file already uses for seeding).
2712        let design =
2713            compile("module m;\n  specparam tRise = 10;\n  specparam tFall = 22;\nendmodule\n");
2714        let body = top_body(&design);
2715        let mut model = TimingModel::new();
2716        model.seed_specparams(&[(InstanceId(0), body)]);
2717        let uid = body
2718            .members()
2719            .find(|s| s.name() == "tRise")
2720            .expect("tRise member")
2721            .uid();
2722        assert_eq!(
2723            model
2724                .specparam(InstanceId(0), &uid)
2725                .and_then(ConstantValue::as_i64),
2726            Some(10)
2727        );
2728
2729        let new_value = body
2730            .members()
2731            .find(|s| s.name() == "tFall")
2732            .expect("tFall member")
2733            .constant_value()
2734            .expect("tFall has a constant value");
2735        model.set_specparam(InstanceId(0), uid.clone(), new_value);
2736
2737        assert_eq!(
2738            model
2739                .specparam(InstanceId(0), &uid)
2740                .and_then(ConstantValue::as_i64),
2741            Some(22)
2742        );
2743    }
2744
2745    #[test]
2746    fn seed_specparams_finds_specparams_declared_inside_a_specify_block() {
2747        // Review fix (r1): a `specparam` declared INSIDE `specify ...
2748        // endspecify` (LRM 30.3's own `specify_item` production lists
2749        // `specparam_declaration`) is legal, and is the ONLY legal location
2750        // for a `PATHPULSE$` specparam (30.7.1). Its real `Specparam`
2751        // symbol is a child of the `SpecifyBlock` member, never a direct
2752        // `Specparam` member of the module body the way a module-scope one
2753        // is -- `seed_specparams` must still find it.
2754        let design = compile(
2755            "module m(input a, output q);\n  assign q = a;\n  specify\n    (a => q) = tRise;\n   \
2756             specparam tRise = 10;\n    specparam PATHPULSE$ = 3;\n  endspecify\nendmodule\n",
2757        );
2758        let body = top_body(&design);
2759        let mut model = TimingModel::new();
2760        model.seed_specparams(&[(InstanceId(0), body)]);
2761
2762        let specify = body
2763            .members()
2764            .find(|s| s.kind() == SymbolKind::SpecifyBlock)
2765            .expect("specify block member");
2766        let rise_uid = specify
2767            .members()
2768            .find(|s| s.kind() == SymbolKind::Specparam && s.name() == "tRise")
2769            .expect("tRise member inside the specify block")
2770            .uid();
2771        let pulse_uid = specify
2772            .members()
2773            .find(|s| s.kind() == SymbolKind::Specparam && s.name() == "PATHPULSE$")
2774            .expect("PATHPULSE$ member inside the specify block")
2775            .uid();
2776
2777        assert_eq!(
2778            model
2779                .specparam(InstanceId(0), &rise_uid)
2780                .and_then(ConstantValue::as_i64),
2781            Some(10),
2782            "a plain specparam declared inside the specify block must be seeded"
2783        );
2784        assert_eq!(
2785            model
2786                .specparam(InstanceId(0), &pulse_uid)
2787                .and_then(ConstantValue::as_i64),
2788            Some(3),
2789            "a PATHPULSE$ specparam -- legal only inside a specify block (30.7.1) -- must be \
2790             seeded too"
2791        );
2792    }
2793
2794    // Task 4a: `PathDelaySet::from_list`'s 1/2/3-value forms (LRM Table
2795    // 30-2) and Table 30-3's own x-transition default algorithm. T5 adds
2796    // the 6/12-value worked examples below; these pin the basic shape
2797    // T4a's own `PathRetimer` actually reads.
2798
2799    #[test]
2800    fn from_list_one_value_gives_every_known_transition_the_same_delay() {
2801        let d = PathDelaySet::from_list(&[10]).expect("a 1-value list is supported");
2802        for (from, to) in [
2803            (Bit4::Zero, Bit4::One),
2804            (Bit4::One, Bit4::Zero),
2805            (Bit4::Zero, Bit4::Z),
2806            (Bit4::Z, Bit4::One),
2807            (Bit4::One, Bit4::Z),
2808            (Bit4::Z, Bit4::Zero),
2809        ] {
2810            assert_eq!(d.of(from, to), 10, "{from:?} -> {to:?}");
2811        }
2812    }
2813
2814    #[test]
2815    fn from_list_two_values_split_rise_and_fall_and_z_follows_the_matching_side() {
2816        let d = PathDelaySet::from_list(&[3, 5]).expect("a 2-value list is supported");
2817        assert_eq!(d.of(Bit4::Zero, Bit4::One), 3, "0->1 is trise");
2818        assert_eq!(d.of(Bit4::Z, Bit4::One), 3, "z->1 is trise");
2819        assert_eq!(d.of(Bit4::One, Bit4::Zero), 5, "1->0 is tfall");
2820        assert_eq!(d.of(Bit4::Z, Bit4::Zero), 5, "z->0 is tfall");
2821        assert_eq!(d.of(Bit4::Zero, Bit4::Z), 3, "0->z is trise");
2822        assert_eq!(d.of(Bit4::One, Bit4::Z), 5, "1->z is tfall");
2823    }
2824
2825    #[test]
2826    fn from_list_three_values_give_z_its_own_delay_distinct_from_rise_and_fall() {
2827        let d = PathDelaySet::from_list(&[3, 5, 7]).expect("a 3-value list is supported");
2828        assert_eq!(d.of(Bit4::Zero, Bit4::One), 3);
2829        assert_eq!(d.of(Bit4::One, Bit4::Zero), 5);
2830        assert_eq!(d.of(Bit4::Zero, Bit4::Z), 7, "0->z is tz, not trise");
2831        assert_eq!(d.of(Bit4::One, Bit4::Z), 7, "1->z is tz, not tfall");
2832        // z->1/z->0 still follow rise/fall, per Table 30-2's own 3-value
2833        // column (only 0->z/1->z use the third value).
2834        assert_eq!(d.of(Bit4::Z, Bit4::One), 3);
2835        assert_eq!(d.of(Bit4::Z, Bit4::Zero), 5);
2836    }
2837
2838    #[test]
2839    fn from_list_derives_x_transitions_via_table_30_3s_general_algorithm() {
2840        // Table 30-3's own general algorithm (`s->x` = min of the two
2841        // delays INTO x from a known state; `x->s` = max of the two delays
2842        // OUT of x into a known state), exercised over a 3-value list's own
2843        // six known transitions (r01=5, r10=12, r0z=17, rz1=r01=5,
2844        // r1z=r0z=17, rz0=r10=12 -- Table 30-2's own 3-value column).
2845        // `from_list_six_value_reproduces_the_lrm_worked_example_exactly`
2846        // below reproduces the LRM's own literal 6-value worked example
2847        // (30.5.2) through the SAME algorithm; this pins it against the
2848        // 3-value shape T4a actually built it for.
2849        let d = PathDelaySet::from_list(&[5, 12, 17]).expect("a 3-value list is supported");
2850        // r01=5, r10=12, r0z=17, rz1=5 (=r01), r1z=17 (=r0z), rz0=12 (=r10).
2851        assert_eq!(
2852            d.of(Bit4::Zero, Bit4::X),
2853            5,
2854            "0->x = min(0->z=17, 0->1=5) = 5"
2855        );
2856        assert_eq!(
2857            d.of(Bit4::One, Bit4::X),
2858            12,
2859            "1->x = min(1->z=17, 1->0=12) = 12"
2860        );
2861        assert_eq!(d.of(Bit4::Z, Bit4::X), 5, "z->x = min(z->1=5, z->0=12) = 5");
2862        assert_eq!(
2863            d.of(Bit4::X, Bit4::Zero),
2864            12,
2865            "x->0 = max(z->0=12, 1->0=12) = 12"
2866        );
2867        assert_eq!(
2868            d.of(Bit4::X, Bit4::One),
2869            5,
2870            "x->1 = max(z->1=5, 0->1=5) = 5"
2871        );
2872        assert_eq!(
2873            d.of(Bit4::X, Bit4::Z),
2874            17,
2875            "x->z = max(1->z=17, 0->z=17) = 17"
2876        );
2877    }
2878
2879    // Task 5: `PathDelaySet::from_list`'s 6- and 12-value forms (LRM Table
2880    // 30-2) and the x-transition default algorithm (Table 30-3) applied to
2881    // them.
2882
2883    #[test]
2884    fn from_list_one_value_gives_all_twelve_entries_the_same_delay() {
2885        // The 1-value test above only pins the six "known" transitions;
2886        // this one pins Table 30-3's algorithm degenerating correctly too
2887        // (s->x = min(s->s, s->s) = s; x->s = max(s->s, s->s) = s), so
2888        // EVERY one of the 12 entries -- not just the six Table 30-2 names
2889        // directly -- is the single given value.
2890        let d = PathDelaySet::from_list(&[10]).expect("a 1-value list is supported");
2891        for (from, to) in [
2892            (Bit4::Zero, Bit4::One),
2893            (Bit4::One, Bit4::Zero),
2894            (Bit4::Zero, Bit4::Z),
2895            (Bit4::Z, Bit4::One),
2896            (Bit4::One, Bit4::Z),
2897            (Bit4::Z, Bit4::Zero),
2898            (Bit4::Zero, Bit4::X),
2899            (Bit4::X, Bit4::One),
2900            (Bit4::One, Bit4::X),
2901            (Bit4::X, Bit4::Zero),
2902            (Bit4::X, Bit4::Z),
2903            (Bit4::Z, Bit4::X),
2904        ] {
2905            assert_eq!(d.of(from, to), 10, "{from:?} -> {to:?}");
2906        }
2907    }
2908
2909    #[test]
2910    fn from_list_six_value_list_takes_each_known_transition_directly_no_grouping() {
2911        // Six DISTINCT values, so a wrong implementation that reused the
2912        // 1/2/3-value forms' rise/fall/z grouping (forcing tz1==t01,
2913        // t1z==t0z, tz0==t10) would fail this test, unlike the LRM's own
2914        // worked example below where that grouping happens to coincide by
2915        // construction for some of the pairs.
2916        let d = PathDelaySet::from_list(&[1, 2, 3, 4, 5, 6]).expect("a 6-value list is supported");
2917        assert_eq!(d.of(Bit4::Zero, Bit4::One), 1, "t01");
2918        assert_eq!(d.of(Bit4::One, Bit4::Zero), 2, "t10");
2919        assert_eq!(d.of(Bit4::Zero, Bit4::Z), 3, "t0z");
2920        assert_eq!(d.of(Bit4::Z, Bit4::One), 4, "tz1 -- independent of t01");
2921        assert_eq!(d.of(Bit4::One, Bit4::Z), 5, "t1z -- independent of t0z");
2922        assert_eq!(d.of(Bit4::Z, Bit4::Zero), 6, "tz0 -- independent of t10");
2923    }
2924
2925    #[test]
2926    fn from_list_six_value_reproduces_the_lrm_worked_example_exactly() {
2927        // LRM 1800-2023 30.5.2's own literal worked example: `(C => Q) =
2928        // (5, 12, 17, 10, 6, 22);` -> `0->x=5, 1->x=6, z->x=10, x->0=22,
2929        // x->1=10, x->z=17` (verified directly against the LRM text,
2930        // Table 30-3's own "Usage" rows).
2931        let d =
2932            PathDelaySet::from_list(&[5, 12, 17, 10, 6, 22]).expect("a 6-value list is supported");
2933        // The six given transitions themselves (Table 30-2's 6-value
2934        // column: t01, t10, t0z, tz1, t1z, tz0, in that order).
2935        assert_eq!(d.of(Bit4::Zero, Bit4::One), 5, "t01");
2936        assert_eq!(d.of(Bit4::One, Bit4::Zero), 12, "t10");
2937        assert_eq!(d.of(Bit4::Zero, Bit4::Z), 17, "t0z");
2938        assert_eq!(d.of(Bit4::Z, Bit4::One), 10, "tz1");
2939        assert_eq!(d.of(Bit4::One, Bit4::Z), 6, "t1z");
2940        assert_eq!(d.of(Bit4::Z, Bit4::Zero), 22, "tz0");
2941        // The six derived x-transitions, matching the LRM's own stated
2942        // numbers exactly.
2943        assert_eq!(d.of(Bit4::Zero, Bit4::X), 5, "0->x = min(17, 5) = 5");
2944        assert_eq!(d.of(Bit4::One, Bit4::X), 6, "1->x = min(6, 12) = 6");
2945        assert_eq!(d.of(Bit4::Z, Bit4::X), 10, "z->x = min(10, 22) = 10");
2946        assert_eq!(d.of(Bit4::X, Bit4::Zero), 22, "x->0 = max(22, 12) = 22");
2947        assert_eq!(d.of(Bit4::X, Bit4::One), 10, "x->1 = max(10, 5) = 10");
2948        assert_eq!(d.of(Bit4::X, Bit4::Z), 17, "x->z = max(6, 17) = 17");
2949    }
2950
2951    #[test]
2952    fn from_list_twelve_value_list_is_all_explicit_no_derivation() {
2953        // Twelve DISTINCT values, in Table 30-2's own 12-value column order
2954        // (t01,t10,t0z,tz1,t1z,tz0,t0x,tx1,t1x,tx0,txz,tzx). If the
2955        // x-involving entries were (wrongly) derived via Table 30-3's
2956        // algorithm instead of copied straight through, e.g. `0->x` would
2957        // come out `min(t0z=3, t01=1) = 1`, not the `7` actually given --
2958        // this test fails loudly on that mistake instead of silently
2959        // tolerating it.
2960        let d = PathDelaySet::from_list(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
2961            .expect("a 12-value list is supported");
2962        assert_eq!(d.of(Bit4::Zero, Bit4::One), 1, "t01");
2963        assert_eq!(d.of(Bit4::One, Bit4::Zero), 2, "t10");
2964        assert_eq!(d.of(Bit4::Zero, Bit4::Z), 3, "t0z");
2965        assert_eq!(d.of(Bit4::Z, Bit4::One), 4, "tz1");
2966        assert_eq!(d.of(Bit4::One, Bit4::Z), 5, "t1z");
2967        assert_eq!(d.of(Bit4::Z, Bit4::Zero), 6, "tz0");
2968        assert_eq!(
2969            d.of(Bit4::Zero, Bit4::X),
2970            7,
2971            "t0x -- given directly, not derived"
2972        );
2973        assert_eq!(
2974            d.of(Bit4::X, Bit4::One),
2975            8,
2976            "tx1 -- given directly, not derived"
2977        );
2978        assert_eq!(
2979            d.of(Bit4::One, Bit4::X),
2980            9,
2981            "t1x -- given directly, not derived"
2982        );
2983        assert_eq!(
2984            d.of(Bit4::X, Bit4::Zero),
2985            10,
2986            "tx0 -- given directly, not derived"
2987        );
2988        assert_eq!(
2989            d.of(Bit4::X, Bit4::Z),
2990            11,
2991            "txz -- given directly, not derived"
2992        );
2993        assert_eq!(
2994            d.of(Bit4::Z, Bit4::X),
2995            12,
2996            "tzx -- given directly, not derived"
2997        );
2998    }
2999
3000    #[test]
3001    fn from_list_rejects_any_length_other_than_1_2_3_6_or_12() {
3002        for len in [4, 5, 7, 8, 9, 10, 11, 13] {
3003            let d = vec![1u64; len];
3004            let err = PathDelaySet::from_list(&d).unwrap_err();
3005            assert!(
3006                err.contains(&len.to_string()),
3007                "the error must name the bad length, not just say \"unsupported\": {err}"
3008            );
3009        }
3010    }
3011
3012    #[test]
3013    fn set_path_delays_attaches_to_every_desugared_entry_sharing_one_path_uid() {
3014        // `(a, b *> q) = 10;` desugars into two `PathEntry`s (30.4.6)
3015        // sharing one `path_uid`; `set_path_delays` must reach both.
3016        let mut design = compile(
3017            "module m(input a, input b, output q);\n  assign q = a | b;\n  specify\n    (a, b \
3018             *> q) = 10;\n  endspecify\nendmodule\n",
3019        );
3020        let eval = design.eval_session();
3021        let body = top_body(eval.design());
3022        let mut model = TimingModel::new();
3023        model
3024            .discover(&eval, &[(InstanceId(0), body)])
3025            .unwrap_or_else(|e| panic!("discover failed: {e}"));
3026        assert_eq!(model.paths.len(), 2);
3027        let path_uid = model.paths[0].path_uid.clone();
3028        assert_eq!(
3029            model.paths[1].path_uid, path_uid,
3030            "both entries share one path_uid"
3031        );
3032
3033        let delays = PathDelaySet::from_list(&[3, 5]).expect("a 2-value list is supported");
3034        model.set_path_delays(InstanceId(0), &path_uid, delays);
3035        assert_eq!(model.paths[0].delays, delays);
3036        assert_eq!(model.paths[1].delays, delays);
3037    }
3038
3039    const ALL_TRANSITIONS: [(Bit4, Bit4); 12] = [
3040        (Bit4::Zero, Bit4::One),
3041        (Bit4::One, Bit4::Zero),
3042        (Bit4::Zero, Bit4::Z),
3043        (Bit4::Z, Bit4::One),
3044        (Bit4::One, Bit4::Z),
3045        (Bit4::Z, Bit4::Zero),
3046        (Bit4::Zero, Bit4::X),
3047        (Bit4::X, Bit4::One),
3048        (Bit4::One, Bit4::X),
3049        (Bit4::X, Bit4::Zero),
3050        (Bit4::X, Bit4::Z),
3051        (Bit4::Z, Bit4::X),
3052    ];
3053
3054    /// sdf T10 (LRM 30.7): with no `PATHPULSE$`, every transition's reject
3055    /// AND error limit is that transition's OWN delay (here a 12-value list
3056    /// with twelve distinct values, so a limit that merely copied one
3057    /// transition's delay everywhere would be caught).
3058    #[test]
3059    fn pulse_limits_default_to_each_transitions_own_delay() {
3060        let mut design = compile(
3061            "module m(input a, output q);\n  assign q = a;\n  specify\n    (a => q) = 10;\n  \
3062             endspecify\nendmodule\n",
3063        );
3064        let eval = design.eval_session();
3065        let body = top_body(eval.design());
3066        let mut model = TimingModel::new();
3067        model
3068            .discover(&eval, &[(InstanceId(0), body)])
3069            .unwrap_or_else(|e| panic!("discover failed: {e}"));
3070        let path_uid = model.paths[0].path_uid.clone();
3071        let delays = PathDelaySet::from_list(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
3072            .expect("a 12-value list is supported");
3073        model.set_path_delays(InstanceId(0), &path_uid, delays);
3074        for (from, to) in ALL_TRANSITIONS {
3075            let d = delays.of(from, to);
3076            assert_eq!(
3077                model.paths[0].reject_limit.of(from, to),
3078                d,
3079                "{from:?}->{to:?}"
3080            );
3081            assert_eq!(
3082                model.paths[0].error_limit.of(from, to),
3083                d,
3084                "{from:?}->{to:?}"
3085            );
3086        }
3087        assert_eq!(
3088            model.paths[0].reject_limit.of(Bit4::Zero, Bit4::One),
3089            1,
3090            "twelve DISTINCT limits, not one copied value"
3091        );
3092        assert_eq!(model.paths[0].reject_limit.of(Bit4::Z, Bit4::X), 12);
3093    }
3094
3095    /// sdf T10: `set_path_pulse_limits` reaches every desugared entry of one
3096    /// declaration, sets all twelve transitions, leaves the delays alone,
3097    /// and raises an error limit below the reject limit up to it (the
3098    /// documented implementation-defined choice).
3099    #[test]
3100    fn set_path_pulse_limits_reaches_every_entry_and_clamps_error_to_reject() {
3101        let mut design = compile(
3102            "module m(input a, input b, output q);\n  assign q = a | b;\n  specify\n    (a, b \
3103             *> q) = 10;\n  endspecify\nendmodule\n",
3104        );
3105        let eval = design.eval_session();
3106        let body = top_body(eval.design());
3107        let mut model = TimingModel::new();
3108        model
3109            .discover(&eval, &[(InstanceId(0), body)])
3110            .unwrap_or_else(|e| panic!("discover failed: {e}"));
3111        let path_uid = model.paths[0].path_uid.clone();
3112        let delays = PathDelaySet::from_list(&[10]).expect("a 1-value list is supported");
3113        model.set_path_delays(InstanceId(0), &path_uid, delays);
3114
3115        model.set_path_pulse_limits(InstanceId(0), &path_uid, 2, 6);
3116        for p in &model.paths {
3117            for (from, to) in ALL_TRANSITIONS {
3118                assert_eq!(p.reject_limit.of(from, to), 2);
3119                assert_eq!(p.error_limit.of(from, to), 6);
3120            }
3121            assert_eq!(p.delays, delays, "pulse limits never touch the delays");
3122        }
3123
3124        model.set_path_pulse_limits(InstanceId(0), &path_uid, 6, 2);
3125        for p in &model.paths {
3126            assert_eq!(p.reject_limit.of(Bit4::Zero, Bit4::One), 6);
3127            assert_eq!(
3128                p.error_limit.of(Bit4::Zero, Bit4::One),
3129                6,
3130                "error < reject is raised to the reject limit"
3131            );
3132        }
3133
3134        // Re-annotating the delays re-establishes the delay-following default.
3135        let longer = PathDelaySet::from_list(&[20]).expect("a 1-value list is supported");
3136        model.set_path_delays(InstanceId(0), &path_uid, longer);
3137        assert_eq!(model.paths[0].reject_limit, longer);
3138        assert_eq!(model.paths[1].error_limit, longer);
3139    }
3140
3141    /// sdf T31 (LRM 30.7.2, 32.7): the global percentage options set every transition's limit to
3142    /// that percentage of ITS OWN delay, and an error limit below the reject limit is raised to it.
3143    #[test]
3144    fn apply_pulse_percent_scales_each_transitions_own_delay() {
3145        let mut design = compile(
3146            "module m(input a, input b, output q);\n  assign q = a | b;\n  specify\n    (a, b \
3147             *> q) = 10;\n  endspecify\nendmodule\n",
3148        );
3149        let eval = design.eval_session();
3150        let body = top_body(eval.design());
3151        let mut model = TimingModel::new();
3152        model
3153            .discover(&eval, &[(InstanceId(0), body)])
3154            .unwrap_or_else(|e| panic!("discover failed: {e}"));
3155        let path_uid = model.paths[0].path_uid.clone();
3156        let delays = PathDelaySet::from_list(&[10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120])
3157            .expect("a 12-value list is supported");
3158        model.set_path_delays(InstanceId(0), &path_uid, delays);
3159
3160        model.apply_pulse_percent(50.0, 25.0);
3161        for p in &model.paths {
3162            assert_eq!(p.delays, delays, "limits never touch the delays");
3163            for (from, to) in ALL_TRANSITIONS {
3164                let d = delays.of(from, to);
3165                assert_eq!(
3166                    p.reject_limit.of(from, to),
3167                    d / 2,
3168                    "reject {from:?}->{to:?}"
3169                );
3170                assert_eq!(
3171                    p.error_limit.of(from, to),
3172                    d / 2,
3173                    "error 25% < reject 50% is raised to the reject limit"
3174                );
3175            }
3176        }
3177
3178        model.apply_pulse_percent(20.0, 90.0);
3179        for p in &model.paths {
3180            for (from, to) in ALL_TRANSITIONS {
3181                let d = delays.of(from, to);
3182                assert_eq!(p.reject_limit.of(from, to), d / 5);
3183                assert_eq!(p.error_limit.of(from, to), d * 9 / 10);
3184            }
3185        }
3186
3187        // 100/100 is the LRM default: limits equal the delays, exactly.
3188        model.apply_pulse_percent(100.0, 100.0);
3189        assert_eq!(model.paths[0].reject_limit, delays);
3190        assert_eq!(model.paths[1].error_limit, delays);
3191
3192        // Rounds to the nearest tick (3 * 50% = 1.5 -> 2).
3193        let odd = PathDelaySet::from_list(&[3]).expect("a 1-value list is supported");
3194        model.set_path_delays(InstanceId(0), &path_uid, odd);
3195        model.apply_pulse_percent(50.0, 100.0);
3196        assert_eq!(model.paths[0].reject_limit.of(Bit4::Zero, Bit4::One), 2);
3197        assert_eq!(model.paths[0].error_limit.of(Bit4::Zero, Bit4::One), 3);
3198    }
3199
3200    /// sdf T31 (LRM 30.7.4.1/30.7.4.2): "The pulse style invocation options take precedence over
3201    /// pulse style specify block declarations" and the same for showcancelled; an axis whose
3202    /// option was not given keeps the declarations, each axis on its own.
3203    #[test]
3204    fn pulse_options_override_declarations_and_an_absent_option_leaves_them() {
3205        let mut design = compile(
3206            "module m(input a, output q, output r, output s);\n  assign q = a;\n  assign r = a;\n  \
3207             assign s = a;\n  specify\n    pulsestyle_ondetect q;\n    showcancelled q;\n    \
3208             noshowcancelled r;\n    (a => q) = 1;\n    (a => r) = 1;\n    (a => s) = 1;\n  endspecify\nendmodule\n",
3209        );
3210        let eval = design.eval_session();
3211        let body = top_body(eval.design());
3212        let settings = |model: &TimingModel| -> Vec<(PulseStyle, bool)> {
3213            ["q", "r", "s"]
3214                .iter()
3215                .map(|out| {
3216                    let p = model
3217                        .paths
3218                        .iter()
3219                        .find(|p| p.outputs[0].0.path().rsplit('.').next() == Some(*out))
3220                        .unwrap_or_else(|| panic!("no path to {out}"));
3221                    (p.pulse_style, p.showcancelled)
3222                })
3223                .collect()
3224        };
3225
3226        let mut plain = TimingModel::new();
3227        plain
3228            .discover(&eval, &[(InstanceId(0), body)])
3229            .unwrap_or_else(|e| panic!("discover failed: {e}"));
3230        assert_eq!(
3231            settings(&plain),
3232            vec![
3233                // q: declared on-detect and showcancelled; r: declared off; s: built-in default.
3234                (PulseStyle::OnDetect, true),
3235                (PulseStyle::OnEvent, false),
3236                (PulseStyle::OnEvent, false)
3237            ],
3238            "declarations, then the built-in default: on-event, noshowcancelled"
3239        );
3240
3241        let discovered = |style: Option<PulseStyle>, show: Option<bool>| {
3242            let mut model = TimingModel::new();
3243            model.set_pulse_options(style, show);
3244            model
3245                .discover(&eval, &[(InstanceId(0), body)])
3246                .unwrap_or_else(|e| panic!("discover failed: {e}"));
3247            settings(&model)
3248        };
3249        use PulseStyle::{OnDetect, OnEvent};
3250        // Both options: they beat q's `pulsestyle_onevent` and r's `noshowcancelled`.
3251        assert_eq!(
3252            discovered(Some(OnDetect), Some(true)),
3253            vec![(OnDetect, true), (OnDetect, true), (OnDetect, true)]
3254        );
3255        // Style option only: showcancelled keeps the declarations (q on, r off, s default).
3256        assert_eq!(
3257            discovered(Some(OnEvent), None),
3258            vec![(OnEvent, true), (OnEvent, false), (OnEvent, false)]
3259        );
3260        // `--noshowcancelled` forces off what q's declaration turned on, and the style axis keeps
3261        // q's declaration.
3262        assert_eq!(
3263            discovered(None, Some(false)),
3264            vec![(OnDetect, false), (OnEvent, false), (OnEvent, false)]
3265        );
3266        // `--showcancelled` forces on what r's `noshowcancelled` declared off.
3267        assert_eq!(
3268            discovered(None, Some(true)),
3269            vec![(OnDetect, true), (OnEvent, true), (OnEvent, true)]
3270        );
3271    }
3272
3273    // Task 4a review fix (r1): LRM §30.4.7's own polarity -- the path
3274    // operator's `+`/`-` -- round-trips into `PathEntry::polarity`, readable
3275    // via the debug/VPI-facing `PathEntry::debug_polarity` accessor.
3276    // `sv-sim-bin::specify`'s own
3277    // `path_retimer_never_reads_polarity_in_its_delay_computation` test
3278    // proves the OTHER half of this task's acceptance text: that nothing in
3279    // the retiming math ever consults it.
3280
3281    #[test]
3282    fn discover_resolves_positive_polarity_on_a_full_connection_path() {
3283        use sv_lang::TimingPathPolarity;
3284
3285        let mut design = compile(
3286            "module m(input a, output q);\n  assign q = a;\n  specify\n    (a +*> q) = 1;\n  \
3287             endspecify\nendmodule\n",
3288        );
3289        let eval = design.eval_session();
3290        let body = top_body(eval.design());
3291        let mut model = TimingModel::new();
3292        model
3293            .discover(&eval, &[(InstanceId(0), body)])
3294            .unwrap_or_else(|e| panic!("discover failed: {e}"));
3295        assert_eq!(model.paths.len(), 1);
3296        assert_eq!(
3297            model.paths[0].debug_polarity(),
3298            TimingPathPolarity::Positive
3299        );
3300    }
3301
3302    #[test]
3303    fn discover_resolves_negative_polarity_on_a_parallel_connection_path() {
3304        use sv_lang::TimingPathPolarity;
3305
3306        let mut design = compile(
3307            "module m(input a, output q);\n  assign q = a;\n  specify\n    (a -=> q) = 1;\n  \
3308             endspecify\nendmodule\n",
3309        );
3310        let eval = design.eval_session();
3311        let body = top_body(eval.design());
3312        let mut model = TimingModel::new();
3313        model
3314            .discover(&eval, &[(InstanceId(0), body)])
3315            .unwrap_or_else(|e| panic!("discover failed: {e}"));
3316        assert_eq!(model.paths.len(), 1);
3317        assert_eq!(
3318            model.paths[0].debug_polarity(),
3319            TimingPathPolarity::Negative
3320        );
3321    }
3322
3323    #[test]
3324    fn discover_resolves_unknown_polarity_when_no_polarity_operator_is_written() {
3325        use sv_lang::TimingPathPolarity;
3326
3327        let mut design = compile(
3328            "module m(input a, output q);\n  assign q = a;\n  specify\n    (a => q) = 1;\n  \
3329             endspecify\nendmodule\n",
3330        );
3331        let eval = design.eval_session();
3332        let body = top_body(eval.design());
3333        let mut model = TimingModel::new();
3334        model
3335            .discover(&eval, &[(InstanceId(0), body)])
3336            .unwrap_or_else(|e| panic!("discover failed: {e}"));
3337        assert_eq!(model.paths.len(), 1);
3338        assert_eq!(model.paths[0].debug_polarity(), TimingPathPolarity::Unknown);
3339    }
3340
3341    // Task 7 (LRM 30.4.3, edge-sensitive paths): `PathEdge` derivation --
3342    // verified LIVE (not assumed) against sv-lang=0.1.9/slang 11.0 that
3343    // `timing_path_edge_identifier()` ALONE cannot tell a plain path apart
3344    // from an edge-sensitive one with no edge keyword (both report
3345    // `EdgeKind::None`), and that the identifier is populated even for the
3346    // edge-sensitive grammar's "simple destination" alternative (no
3347    // `(dest:data)` suffix at all) -- see `PathEdge`'s own doc comment for
3348    // the full four-case matrix this pins.
3349
3350    #[test]
3351    fn discover_resolves_specific_posedge_on_an_edge_sensitive_path() {
3352        use sv_lang::EdgeKind;
3353
3354        let mut design = compile(
3355            "module m(input clock, in, output out);\n  specify\n    (posedge clock => (out +: \
3356             in)) = (10, 8);\n  endspecify\nendmodule\n",
3357        );
3358        let eval = design.eval_session();
3359        let body = top_body(eval.design());
3360        let mut model = TimingModel::new();
3361        model
3362            .discover(&eval, &[(InstanceId(0), body)])
3363            .unwrap_or_else(|e| panic!("discover failed: {e}"));
3364        assert_eq!(model.paths.len(), 1);
3365        assert_eq!(model.paths[0].edge, PathEdge::Specific(EdgeKind::PosEdge));
3366        assert_eq!(
3367            model.paths[0].debug_edge_source_expr_name(),
3368            Some("in"),
3369            "the `: in` data-source expression's referenced symbol name must round-trip"
3370        );
3371    }
3372
3373    #[test]
3374    fn discover_resolves_specific_negedge_on_an_edge_sensitive_path() {
3375        use sv_lang::EdgeKind;
3376
3377        let mut design = compile(
3378            "module m(input clock, in, output out);\n  specify\n    (negedge clock => (out -: \
3379             in)) = (10, 8);\n  endspecify\nendmodule\n",
3380        );
3381        let eval = design.eval_session();
3382        let body = top_body(eval.design());
3383        let mut model = TimingModel::new();
3384        model
3385            .discover(&eval, &[(InstanceId(0), body)])
3386            .unwrap_or_else(|e| panic!("discover failed: {e}"));
3387        assert_eq!(model.paths.len(), 1);
3388        assert_eq!(model.paths[0].edge, PathEdge::Specific(EdgeKind::NegEdge));
3389    }
3390
3391    #[test]
3392    fn discover_resolves_edge_sensitive_any_transition_when_no_edge_identifier_is_written() {
3393        // LRM 30.4.3's own Example 3: `(clock => (out : in)) = (10, 8);` --
3394        // no edge keyword at all, but the `(dest:data)` destination suffix
3395        // marks this as edge-sensitive (`PathEdge::Any`), not a plain
3396        // `PathEdge::None` simple path -- `timing_path_edge_identifier()`
3397        // alone reports `EdgeKind::None` for BOTH, which is exactly why
3398        // `PathEdge::from_accessors` also consults
3399        // `timing_path_edge_source_expr()`.
3400        let mut design = compile(
3401            "module m(input clock, in, output out);\n  specify\n    (clock => (out : in)) = \
3402             (10, 8);\n  endspecify\nendmodule\n",
3403        );
3404        let eval = design.eval_session();
3405        let body = top_body(eval.design());
3406        let mut model = TimingModel::new();
3407        model
3408            .discover(&eval, &[(InstanceId(0), body)])
3409            .unwrap_or_else(|e| panic!("discover failed: {e}"));
3410        assert_eq!(model.paths.len(), 1);
3411        assert_eq!(model.paths[0].edge, PathEdge::Any);
3412    }
3413
3414    #[test]
3415    fn discover_resolves_no_edge_sensitivity_on_a_plain_simple_path() {
3416        // The negative case: a plain 30.4.2 simple path (no edge-sensitive
3417        // suffix at all) must resolve `PathEdge::None`, not `PathEdge::Any`
3418        // -- `edge_source_expr().is_some()` must be `false` here, the
3419        // discriminator `PathEdge::from_accessors` relies on.
3420        let mut design = compile(
3421            "module m(input a, output q);\n  assign q = a;\n  specify\n    (a => q) = 1;\n  \
3422             endspecify\nendmodule\n",
3423        );
3424        let eval = design.eval_session();
3425        let body = top_body(eval.design());
3426        let mut model = TimingModel::new();
3427        model
3428            .discover(&eval, &[(InstanceId(0), body)])
3429            .unwrap_or_else(|e| panic!("discover failed: {e}"));
3430        assert_eq!(model.paths.len(), 1);
3431        assert_eq!(model.paths[0].edge, PathEdge::None);
3432        assert_eq!(model.paths[0].debug_edge_source_expr_name(), None);
3433    }
3434
3435    #[test]
3436    fn discover_resolves_specific_posedge_even_with_the_simple_destination_suffix() {
3437        // `(posedge clk => q) = 1;` -- edge keyword given, but NO
3438        // `(dest:data)` suffix at all (`edge_source_expr()` is `None` here):
3439        // `timing_path_edge_identifier()` is still populated, proving the
3440        // identifier is a field of the shared `[edge_identifier]`
3441        // production, independent of which destination-suffix alternative
3442        // is used -- `PathEdge::from_accessors` must still resolve
3443        // `Specific(PosEdge)`, not fall through to `None`.
3444        use sv_lang::EdgeKind;
3445
3446        let mut design = compile(
3447            "module m(input clk, output q);\n  specify\n    (posedge clk => q) = 1;\n  \
3448             endspecify\nendmodule\n",
3449        );
3450        let eval = design.eval_session();
3451        let body = top_body(eval.design());
3452        let mut model = TimingModel::new();
3453        model
3454            .discover(&eval, &[(InstanceId(0), body)])
3455            .unwrap_or_else(|e| panic!("discover failed: {e}"));
3456        assert_eq!(model.paths.len(), 1);
3457        assert_eq!(model.paths[0].edge, PathEdge::Specific(EdgeKind::PosEdge));
3458        assert_eq!(model.paths[0].debug_edge_source_expr_name(), None);
3459    }
3460
3461    #[test]
3462    fn discover_shares_one_edge_spec_across_every_desugared_entry_of_a_full_connection() {
3463        // LRM 30.4.6's own desugaring: a multi-terminal FULL (`*>`)
3464        // edge-sensitive declaration shares its one `edge`/data-source name
3465        // across every desugared `PathEntry`, exactly like it already
3466        // shares one `path_uid`/`polarity`/delay list.
3467        let mut design = compile(
3468            "module m(input clock, input a, input b, output q1, output q2);\n  specify\n    \
3469             (posedge clock *> (q1, q2 : a)) = (10, 8);\n  endspecify\nendmodule\n",
3470        );
3471        let eval = design.eval_session();
3472        let body = top_body(eval.design());
3473        let mut model = TimingModel::new();
3474        model
3475            .discover(&eval, &[(InstanceId(0), body)])
3476            .unwrap_or_else(|e| panic!("discover failed: {e}"));
3477        assert_eq!(
3478            model.paths.len(),
3479            2,
3480            "two desugared (clock*>q1), (clock*>q2) entries"
3481        );
3482        for p in &model.paths {
3483            assert_eq!(p.edge, PathEdge::Specific(sv_lang::EdgeKind::PosEdge));
3484            assert_eq!(p.debug_edge_source_expr_name(), Some("a"));
3485        }
3486    }
3487}