vj_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, `vj::specify`/
7//! `vj::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//! `vj-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 vj_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; `vj::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 (`vj::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 `vj::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; `vj::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 (`vj::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. `vj::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. `vj`'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 `vj`'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 `vj-codegen::
331 /// timescale` (unit/precision conversion); this crate's own `frontend`
332 /// feature carries the former but can never depend on the latter
333 /// (`vj-codegen` depends on `vj-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 /// `vj`'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. [`vj::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 /// `vj::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. `vj::specify::
368 /// PathRetimer::select_delay` reads it (through `vj`'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 /// `vj::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 `vj::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 `vj::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 `vj` 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 `vj` 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 `vj` 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 `vj::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) -- `vj`'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 `vj`) -- 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 `vj::primitives::constant_of`/`vj::
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/// `vj::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 `vj::
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/vj-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 `vj::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 vj_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 // `vj::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}