sv_sim_sim/process/cfg.rs
1//! Task S3a (`docs/superpowers/plans/2026-09-26-wave-A2-slice.md`): the CPS
2//! control-flow-graph builder and its Segment IR lowering, restricted to
3//! exactly the A2-slice's two suspend kinds (`Delay`, `Edge` -- i.e.
4//! `SignalEventControl` with `posedge`/`negedge`). Ledger PR-004, PR-005,
5//! PR-006 (Delay/SignalEvent half only), PR-007, PR-008, PR-041, PR-042,
6//! PR-043, PR-044, PR-048, PR-069, PR-074, PR-114, CG-098, CG-099. See
7//! `docs/design/process.md` §§1-6.
8//!
9//! **Task 1 (`docs/superpowers/plans/2026-09-26-wave-A2-process-kernels.md`,
10//! ledger PR-006/PR-045) extends the above** with the rest of the
11//! suspend-relevant `StatementKind` taxonomy: `Wait` (level-sensitive,
12//! `SuspendPoint::Level` -- CFG shape only, the "sometimes there's no
13//! suspend at all" re-check behavior is Task 3's), `EventTrigger` (split
14//! only when it carries a `DelayControl` timing; an undelayed `->`/`->>`
15//! never suspends), a fork/join `Block` (`Terminator::ForkJoin`,
16//! construction only -- never scheduled, see `docs/design/process.md` §7),
17//! and `Terminator::Loop{header,body,exit,entry}` for `for`/`while`/
18//! `do-while`/`forever`/`foreach` (PR-045). `WaitFork`/`WaitOrder`/
19//! `DisableFork` remain named [`LoweringError`]s (recognized, never silently
20//! ignored, but not yet schedulable -- fork/join has no runtime semantics
21//! this wave either way). Two more `TimingControlKind`s get real handling:
22//! `EventList` (as `SuspendPoint::EdgeList`) and `CycleDelay` (lowered like
23//! `Delay` against a single inferred clock, matching `docs/design/
24//! process.md` §2's own explicitly-flagged stub scope).
25//! `ImplicitEvent`/`RepeatedEvent`/`Delay3`/`OneStepDelay`/`BlockEventList`
26//! remain named `LoweringError`s: `docs/design/process.md` §2's own
27//! `SuspendPoint` enum (the binding design record) has exactly six variants
28//! (`Delay`/`Edge`/`EdgeList`/`ImplicitComb`/`Level`/`CycleDelay`) and does
29//! not admit these five as first-class suspend shapes at all, and this
30//! file's own extension policy (below) forbids adding a placeholder variant
31//! ahead of a real implementation -- so PR-006 stays `"partial"` in
32//! `docs/ledger.json` after this task, not `"done"` (see this task's own
33//! deviations note for the full accounting).
34//!
35//! **Task 6 (`docs/superpowers/plans/2026-09-27-run-wiring.md`, ledger
36//! CG-081/CG-171) closes most of the above gap.** `SuspendPoint` gains real
37//! variants for `ImplicitEvent` (`ImplicitComb`) and `OneStepDelay`
38//! (`OneStep`), and `CycleDelay` is revised to carry its inferred clock
39//! signal/edge as real payload (see that variant's own doc comment for the
40//! full CG-171 reasoning, including a recorded deviation from the plan's
41//! literal `analysis::ClockDomainId`-carrying wire design). Two kinds stay
42//! named `LoweringError`s after this task too, each for a DIFFERENT,
43//! verified reason -- neither is "no `SuspendPoint` variant exists for it"
44//! any more: `BlockEventList` is only a covergroup sampling event (LRM 19.3;
45//! slang rejects it as a procedural timing control at parse, see the
46//! `BlockEventList` arm in `walk_timed`); `RepeatedEvent` is structurally UNREACHABLE at this dispatch
47//! point at all -- verified empirically (a throwaway AST-dumping test) that
48//! `RepeatedEventControl` is never the outer `TimingControl` of a
49//! `StatementKind::Timed` node, only ever an intra-assignment timing control
50//! embedded in an `Assignment` expression's own children, a structurally
51//! different position needing a `Temporary`-frame-slot CFG-splitting
52//! mechanism this AST-only layer does not have -- see the `RepeatedEvent`
53//! arm in `walk_timed` for the full citation and this task's deviations for
54//! the full accounting. `Delay3` remains rejected unconditionally too, but
55//! because it is semantically inapplicable to a procedural suspend point at
56//! all (gate-primitive delay syntax), not a gap.
57//!
58//! **Task 3 (`docs/superpowers/plans/2026-09-26-wave-A2-process-kernels.md`
59//! "A2-part2", ledger CG-169/CG-170/PR-106/CG-115) gives `Wait`/`SignalEvent`
60//! their real, previously-deferred semantics.** [`Self::walk_wait`] no
61//! longer closes `cur` with an unconditional `Terminator::Suspend` -- see its
62//! own doc comment for the `Terminator::Branch`-based "immediate check,
63//! conditional suspend" shape LRM 9.4.3/CG-169 requires. `SuspendPoint::Edge`
64//! gains an `iff: Option<Expression<'d>>` field (CG-170,
65//! `SignalEventControl.iffCondition`), extracted positionally from
66//! `TimingControl::children()` by [`extract_iff`] -- see that function's own
67//! doc comment for the verified-live `sv-lang`=0.1.9 binding shape this
68//! relies on. Neither change touches `SuspendPoint::EdgeList`: Task 1's own
69//! doc comment on that variant already scopes `iff` extraction to the
70//! narrower `(Expression, EdgeKind)` pair with no `iff`, and this task does
71//! not revisit that decision.
72//!
73//! **`disable label;` (`StatementKind::Disable`, distinct from
74//! `DisableFork`).** The pinned `sv-lang`=0.1.9 binding exposes no way to
75//! read `DisableStatement::target`: the C API has no `slang_stmt_disable_*`
76//! accessor, and the generic `children()`/`expressions()` walk never
77//! reaches `target` (`DisableStatement` defines no `visitExprs`) -- a
78//! `disable label;` statement's own `children().len()` is `0`. The label is
79//! therefore read from the statement's source span in the design's source
80//! text (registered by the caller, [`set_source_text`]), accepted only when
81//! the span reads exactly `disable <identifier> ;` (see `disable_label`).
82//! A named sequential block containing a `disable` gets its own exit block
83//! (`disable_targets`), and `disable blk;` inside it is a jump there, like
84//! `break` (LRM 1800-2023 §9.6.2). In a branch of the named fork block `f`,
85//! `disable f;` stays a statement (codegen: `sv_rt_disable_fork_block`,
86//! ending every branch of the fork) followed by the branch's end. Any other
87//! target -- another process's block, a task, an outer fork, an unreadable
88//! span -- is a named `LoweringError`.
89//!
90//! **Algorithm (PR-007):** [`build_cfg`] is a single recursive-descent walk
91//! of a process body's `Statement` tree (`children()`/`statements()`/
92//! `then_branch()`/`else_branch()`/`body()`/`condition()`/`expr()`) -- an
93//! ordinary CFG builder -- with the ONE addition that entering a `Timed`
94//! node wrapping a `Delay` or `SignalEvent` (posedge/negedge only) timing
95//! control closes the current [`BasicBlock`] with `Terminator::Suspend` and
96//! opens a fresh block for the guarded statement that follows. Every other
97//! suspend-relevant `StatementKind` (`Wait`, `WaitFork`, `WaitOrder`,
98//! `EventTrigger`, `DisableFork`, a fork/join `Block`) and every other
99//! `TimingControlKind` a `Timed` node could wrap (`EventList`,
100//! `ImplicitEvent`, `RepeatedEvent`, `Delay3`, `OneStepDelay`, `CycleDelay`,
101//! `BlockEventList`) is a named [`LoweringError`], never silently treated as
102//! ordinary control flow (PR-006's slice-scope half) -- Task 1 above lifts
103//! this restriction for `Wait`/`EventTrigger`/fork-join `Block`/`EventList`/
104//! `CycleDelay` specifically.
105//!
106//! **Extension policy (PR-069):** [`Terminator`] and [`SuspendPoint`]
107//! intentionally have exactly the variants a task has actually implemented
108//! -- no placeholder variant is ever added ahead of a real implementation,
109//! because an unused variant would force every `match` in this file (and
110//! every future consumer's) to either wildcard it (forbidden by ARCHITECTURE
111//! §7 item 9) or handle a case that can never actually be constructed (a
112//! "gate that cannot fail", forbidden by this wave's correctness posture).
113//! Instead: a task adding a new suspend kind or terminator shape adds the
114//! variant *and* is required to re-audit every `match` site in this file,
115//! since none has a wildcard arm to silently swallow it -- Task 1 did
116//! exactly this for `Loop`/`ForkJoin`/`EdgeList`/`Level`/`CycleDelay` (but
117//! deliberately NOT `Disable` -- see above). Until a shape has a real
118//! implementation, every code path that *would* need it returns a named
119//! [`LoweringError`] instead. `SegTerminator` (PR-041/042/043,
120//! `lower_terminator` below) deliberately keeps its *own* variant set frozen
121//! at `Goto`/`CondGoto`/`Suspend`/`End` -- it is exhaustively matched by
122//! `sv-sim-codegen` (a different track's contract) with no wildcard arm, so
123//! `Terminator::Loop` lowers losslessly to `SegTerminator::Goto` (a real,
124//! already-populated successor block -- `entry` -- always carries the
125//! actual decision, whether that is `header`'s own `Branch` or a plain
126//! `Fallthrough`) rather than adding a new `SegTerminator` variant that
127//! would break that exhaustive match. A `Terminator::ForkJoin` reaching
128//! `lower_terminator` is a caller-discipline precondition violation (see
129//! [`contains_unschedulable_fork_join`]), not a shape `SegTerminator` can
130//! represent at all this wave -- fork/join has no runtime scheduling
131//! semantics yet, so no `SegTerminator` value would be honest here; every
132//! real caller must check that predicate first.
133use crate::symid::SymbolUid;
134use std::collections::HashSet;
135use std::fmt;
136
137use sv_lang::kinds::{ExpressionKind, StatementKind, SymbolKind, TimingControlKind};
138use sv_lang::{
139 EdgeKind, Expression, KnownSystemName, SemNode, Statement, StatementBlockKind, Symbol,
140 TimingControl, UniquePriorityCheck,
141};
142
143/// Every unhandled AST shape this file's builders encounter produces one of
144/// these -- never `panic!`/`unimplemented!`/a silent no-op. Deliberately a
145/// separate type from `sv-sim-codegen::error::LoweringError`: this crate must
146/// never depend on `sv-sim-codegen` (the dependency edge runs the other way
147/// once Task S3c lands), so it cannot reuse that one.
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct LoweringError {
150 pub message: String,
151}
152
153impl LoweringError {
154 // `pub(crate)` (Task S3b deviation, not `fn`-private as Task S3a left
155 // them): `process::liveness` is a sibling module that needs to construct
156 // the same error shape (declared-local width/shape rejections, malformed
157 // block-scope walks) rather than inventing a second `LoweringError`-like
158 // type just for one module boundary. Still crate-private -- no outside
159 // crate constructs one of these directly, only ever sees it through
160 // `Result::Err`.
161 pub(crate) fn unsupported(what: impl Into<String>) -> Self {
162 Self {
163 message: format!("unsupported: {}", what.into()),
164 }
165 }
166
167 pub(crate) fn malformed(what: impl Into<String>) -> Self {
168 Self {
169 message: format!("malformed AST: {}", what.into()),
170 }
171 }
172
173 /// PR-022's hard, typed self-check failure: `process::liveness::
174 /// compute_liveness`'s backward fixpoint computed a live-in symbol that
175 /// `AnalyzedProcedure::read_set()` (or, absent a live `Analysis` handle,
176 /// this crate's own AST-derived read-set aggregate -- see that module's
177 /// doc comment) never reports as read anywhere in the whole procedure.
178 /// Kept as a named constructor on this same flat `LoweringError` shape
179 /// (deviation from the plan's literal `LoweringError::
180 /// LivenessOracleMismatch` enum-variant phrasing: Task S3a already
181 /// committed `LoweringError` as a message-carrying struct with named
182 /// constructors, not an enum, so this follows that established pattern
183 /// rather than introducing a second, incompatible error type mid-crate)
184 /// -- the message names the offending symbol, so a test asserts on
185 /// content, not just `is_err()`, per this file's own established
186 /// discipline.
187 pub(crate) fn liveness_oracle_mismatch(symbol_name: impl Into<String>) -> Self {
188 Self {
189 message: format!(
190 "LivenessOracleMismatch: computed live-in symbol {:?} is absent from the \
191 whole-procedure read_set -- this is a hard bug in the liveness walk, never \
192 a legitimate program (PR-022)",
193 symbol_name.into()
194 ),
195 }
196 }
197}
198
199impl fmt::Display for LoweringError {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 write!(f, "{}", self.message)
202 }
203}
204
205impl std::error::Error for LoweringError {}
206
207/// Dense, per-process `ProcessCfg::blocks` index, assigned in AST pre-order
208/// (first-appearance-in-source) allocation order (CG-098, CG-099, PR-114) --
209/// completely independent of any later LLVM basic-block layout order, since
210/// no LLVM `Context`/`Module` exists yet when `build_cfg` runs.
211#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
212pub struct BlockId(pub u32);
213
214/// Dense `Segment` index -- see [`BlockId`]'s doc comment; `lower_to_segments`
215/// assigns `SegId(i)` to the block at index `i`, a 1:1, order-preserving
216/// mapping (PR-041..PR-043).
217#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
218pub struct SegId(pub u32);
219
220/// R-run Task 8 (PR-045/PR-006): a `repeat(N) body;` loop's compiler-
221/// synthesized iteration counter, minted once per `RepeatLoop` statement
222/// [`CfgBuilder::walk_repeat_loop`] decomposes into a real CFG shape (i.e.
223/// only when `body` contains a suspend point somewhere in its own subtree --
224/// the suspend-free case stays the pre-existing opaque leaf `stmt_shared::
225/// lower_repeat_loop` already handles unchanged, see `walk_stmt`'s own
226/// dispatch comment). Unlike a `for` loop's user-visible control variable
227/// (a real `sv_lang::Symbol`, `FrameSlotSource::DeclaredVar`), `repeat`'s
228/// count has no corresponding declared local at all -- there is nothing a
229/// `Branch`'s `cond: Expression` could re-evaluate every iteration the way
230/// `while`/`for` do. This id is this crate's own, `sv_lang`-free handle for
231/// that counter's eventual `FrameSlotSource::SyntheticCounter` frame slot
232/// (`process::liveness`) -- dense per-`ProcessCfg`, assigned in allocation
233/// order (mirrors [`BlockId`]/[`SegId`]'s own numbering discipline), never
234/// reused across two distinct `repeat` statements in the same process (so a
235/// nested or sibling `repeat` gets its own, independent counter slot).
236#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
237pub struct RepeatCounterId(pub u32);
238
239/// The suspend kinds this file implements. `Delay`/`Edge` are PR-006's
240/// slice-scope half (Task S3a); `EdgeList`/`Level`/`CycleDelay` are Task 1's
241/// addition (PR-006's Part 2 half). `ImplicitComb` remains unimplemented --
242/// see the module doc comment's PR-006 accounting.
243#[derive(Debug, Clone)]
244pub enum SuspendPoint<'d> {
245 /// `#<delay>`. The second field is the inlined subroutine whose body holds the delay, when
246 /// one does: a delay's time unit is the unit of the scope that DECLARES it (LRM 3.14.2.1),
247 /// not the caller's.
248 Delay(Expression<'d>, Option<Symbol<'d>>),
249 Edge {
250 signal: Expression<'d>,
251 edge: EdgeKind,
252 /// `SignalEventControl.iffCondition` (LRM `@(posedge clk iff en)`,
253 /// Task 3/CG-170): `None` when no `iff` qualifier is present. The
254 /// kernel's resume prologue for a suspend point carrying `Some(_)`
255 /// ALWAYS re-evaluates it on wake and, if false, immediately
256 /// re-returns `Suspended` with the identical `WaitSpec` unchanged --
257 /// it does not advance the PC and does not re-run the segment body
258 /// (`docs/design/process.md` §2's own "iff re-check" paragraph;
259 /// `compile_iff_guard`'s doc comment in `sv-sim-codegen::
260 /// process_lower` is the codegen half of this same contract).
261 iff: Option<Expression<'d>>,
262 },
263 /// `EventListControl` (`@(a or b or c)`): wake on any member. Deviation
264 /// from `docs/design/process.md` §2's literal `Vec<{edge,signal,iff}>`
265 /// triple -- Task 1's own "Public names/types introduced" text specifies
266 /// the narrower `(Expression, EdgeKind)` pair with no `iff` (matching
267 /// how `Level`'s own `iff`/re-check contract is deferred to Task 3);
268 /// this is the more specific, more recent spec and is followed exactly.
269 ///
270 /// The second field holds each member's `iff` guard (LRM 1800-2023
271 /// 9.4.2.3: `@(posedge a iff c or b)`): always the same length as the
272 /// member list, empty for an unguarded member. The list wakes on a
273 /// member's event only if every one of that member's guards holds then
274 /// (more than one when an `iff` meets an `$inferred_clock` whose own
275 /// clocking event has an `iff`, a conjunction: LRM 17.4, 9.4.2.3).
276 EdgeList(Vec<(Expression<'d>, EdgeKind)>, Vec<Vec<Expression<'d>>>),
277 /// `wait(expr)` (LRM 9.4.3, level-sensitive). Task 3: [`CfgBuilder::
278 /// walk_wait`] wires this as the `else_blk` of a `Terminator::Branch` on
279 /// `expr` itself (never unconditionally) -- "evaluate immediately,
280 /// suspend only if false" (`docs/design/process.md` §2's own "wait(expr)
281 /// special case", CG-169). The per-wake re-check (re-suspending
282 /// unchanged on a spurious wake, CG-115) is entirely codegen's job
283 /// (`sv-sim-codegen::process_lower::compile_iff_guard`'s resume
284 /// prologue) -- this crate never evaluates `expr`.
285 Level(Expression<'d>),
286 /// `##N` (clocking-block-relative cycle delay) -- **Task 6 (CG-171)
287 /// revision:** no longer a bare cycle count. ARCHITECTURE §6.5 represents
288 /// `CycleDelayControl` as "kept as an unresolved expression + ClockDomain
289 /// for the scheduler to convert cycles->ticks at runtime", i.e. codegen
290 /// must never precompute an absolute tick offset (a clock's period can
291 /// change dynamically -- see `sched::mod::EventTerm::Repeat`'s own SC-053
292 /// doc comment for the differentiating behavior this rules out). This
293 /// variant carries `count` (the compile-time-constant cycle count --
294 /// still restricted the same way `##N` always was, see
295 /// [`CfgBuilder::walk_timed`]'s `CycleDelay` arm) plus the syntactically
296 /// inferred single clock this process suspends against, `clock`/
297 /// `edge` (or, with a default clocking block in scope, that block
298 /// itself with `BothEdges`: `##N` counts its events) -- the SAME `inferred_clock` this file already tracked before
299 /// this task (previously computed then silently discarded once
300 /// ambiguity-checked; now threaded through as real payload). Resolving
301 /// `clock` to a concrete `StateCellId` still happens at codegen
302 /// time (`sv-sim-codegen::process_lower::resolve_suspend_point`, exactly
303 /// like `Edge`'s own signal) -- what does NOT happen at codegen time is
304 /// converting `count` cycles into an absolute tick number: that stays a
305 /// live, per-edge `EventTerm::Repeat{count, inner: Edge{cell, edge}}`
306 /// wait the SCHEDULER counts down at runtime (already built and tested,
307 /// SC-052/SC-053), so a clock whose period later changes still produces
308 /// the correct absolute timing by construction, with no cached tick
309 /// offset to go stale.
310 ///
311 /// **Deviation from the plan's literal `analysis::ClockDomainId`-carrying
312 /// wire design, and a real, verified SCOPE LIMIT (both recorded in full
313 /// in this task's deviations -- not silently narrowed):** the plan's own
314 /// text describes resolving `clock` against a live `sv-lang`-
315 /// `Analysis`-backed WOCB partition result (`analysis::
316 /// StaticAnalysisResult::clock_domains()`) threaded all the way from
317 /// `sv-sim-bin::run`'s elaboration pipeline through to the scheduler's
318 /// `Engine`. That plumbing does not exist anywhere in this tree yet
319 /// (`analysis::analyze()` is never called from `run.rs`/`aot.rs`/
320 /// `pipeline.rs` today), and building it is a genuinely separate,
321 /// `run.rs`-touching undertaking this task's own binding rules forbid
322 /// risking (wave-B-runtime is concurrently landing in that exact file).
323 /// This variant instead reuses the clock inference this crate ALREADY
324 /// performed for every `##N` before this task (the same
325 /// `inferred_clock` ambiguity check, now threaded through as real
326 /// payload instead of discarded) -- **which is genuinely narrower than
327 /// "the process's own inferred clock domain," not merely a smaller-but-
328 /// equivalent instance of it, and this narrowing is load-bearing, not
329 /// cosmetic:** it resolves `##N` written inside a process that ALSO
330 /// itself syntactically sights a `posedge`/`negedge` edge somewhere
331 /// earlier (the common `always_ff @(posedge clk) ... ##N ...` idiom),
332 /// but does NOT resolve a `##N` relative to a `default clocking`/named
333 /// `clocking cb @(posedge clk); endclocking` block referenced from a
334 /// process with NO local edge sighting of its own -- verified directly
335 /// (a throwaway test compiling ARCHITECTURE §6.5's own canonical
336 /// clocking-block fixture, `default clocking cb; ... initial ##3 x=1;`,
337 /// with no `@(posedge clk)` anywhere in that second `initial` block):
338 /// `walk_timed`'s `CycleDelay` arm returns the SAME "no unambiguous
339 /// single inferred clock in scope" `LoweringError` for it that a
340 /// genuinely unclocked `##N` gets, because `default clocking`/
341 /// `ClockingBlock` declarations are never read by this mechanism at
342 /// all. Reading them is also a `session.rs`-owned, pre-existing,
343 /// verified-separately-blocked gap regardless (`sv-sim-codegen::
344 /// session.rs`'s own structural-symbol classification already places
345 /// `SymbolKind::ClockingBlock` in its "no correct lowering exists yet"
346 /// bucket -- a design containing one fails upstream of this crate
347 /// entirely), so closing it is not a smaller extension of this task's
348 /// own mechanism, it needs a different, cross-track-blocked one. CG-171
349 /// therefore closes for real, common `##N` usage (a clock edge
350 /// syntactically visible in the SAME process) but stays open for the
351 /// `default clocking`-bound shape -- see this task's deviations for the
352 /// full accounting, and `crates/sv-sim-harness/tests/
353 /// slice_vs_iverilog.rs::clocking_cycle_delay_matches_iverilog_bit_for_bit`
354 /// for the still-`#[ignore]`d e2e proof (updated by this task to name
355 /// this exact, narrower residual, not the stale "CG-171 unimplemented"
356 /// reason it carried before). See `sched::reschedule_req_proposal::
357 /// KIND_REPEAT_EDGE`'s own doc comment for the wire shape the resolved
358 /// (local-edge) case resolves into -- a single-cell, single-edge repeat
359 /// wait that fits the frozen 24-byte budget inline (no out-of-line
360 /// table needed, unlike `EdgeList`/`ImplicitComb`).
361 ///
362 /// **Verified, separately-recorded finding (not this task's bug to fix,
363 /// but load-bearing for why no NEW oracle-diffed e2e fixture proves this
364 /// closure end-to-end):** `##N` outside a `default clocking` context is
365 /// not accepted as valid SystemVerilog by either reference tool this
366 /// crate uses -- `iverilog -g2012` rejects a bare `##2` inside an
367 /// ordinary `always_ff` body with a flat parser "syntax error", and
368 /// `verilator --binary -sv` rejects the identical construct with
369 /// "Usage of cycle delays requires default clocking (IEEE 1800-2023
370 /// 14.11)" (both verified directly, by hand, against a throwaway
371 /// fixture). This mechanism's "syntactically inferred single clock"
372 /// acceptance criterion PRE-DATES this task (an earlier task's own
373 /// `inferred_clock` field, which this task only threads through as real
374 /// payload instead of discarding) -- this task neither introduces nor
375 /// revisits that acceptance decision, only documents, empirically, that
376 /// no available reference oracle can differentially validate it end to
377 /// end: the codegen-level JIT+run round trip (`crates/sv-sim-codegen/
378 /// tests/frame_roundtrip_matrix.rs`) is therefore the strongest
379 /// available proof of this specific closure, not an iverilog/Verilator
380 /// diff.
381 CycleDelay {
382 count: u32,
383 /// The clock: a signal (the process's inferred clock, counted on
384 /// `edge`), or the default clocking block (its event counter,
385 /// counted on any change).
386 clock: Symbol<'d>,
387 edge: EdgeKind,
388 /// A clocking drive THROUGH a virtual interface (`v.sb.c <= ##1 1`,
389 /// LRM 25.9.1): the clock variable's member access. `clock` is then
390 /// the template clocking block, which has no signal; the run time
391 /// counts the events of the clocking block of the instance `v` holds.
392 via: Option<Expression<'d>>,
393 },
394 /// **Task 6 (CG-081):** `ImplicitEventControl` (`@*`/`@(*)`). Fieldless
395 /// per `docs/design/process.md` §2's own `SuspendPoint` sketch would
396 /// suggest (`WaitSpec::ImplicitComb(SubscriptionSetId)` is resolved
397 /// against a separate table, not embedded read-set data) -- deviates by
398 /// carrying the guarded body's own AST-derived read-set directly
399 /// (`Vec<Expression<'d>>`, computed by [`implicit_comb_read_set`]) rather
400 /// than being truly fieldless, because this crate's `build_cfg` has no
401 /// live `sv_lang::Analysis`/`AnalyzedProcedure` handle to later re-derive
402 /// it from at codegen time (see [`implicit_comb_read_set`]'s own doc
403 /// comment for the full reasoning) -- the read-set has to be captured
404 /// HERE, while the guarded body is in hand, or nowhere in this crate.
405 ImplicitComb(Vec<Expression<'d>>),
406 /// **Task 6 (CG-081):** `OneStepDelayControl` (the LRM's built-in
407 /// `1step` delay, one global-precision tick, SC-123). Fieldless -- the AST node itself carries no
408 /// expression (confirmed against the vendored slang C++:
409 /// `OneStepDelayControl` declares no field and no `visitExprs` override
410 /// at all, so `TimingControl::children()` is always empty for this
411 /// kind). Resolved at codegen time to `Suspend::Delay(1)` (LRM 3.14.3: a
412 /// step is the global time precision) -- see
413 /// `sv-sim-codegen::process_lower::resolve_suspend_point`'s `OneStep`
414 /// arm.
415 OneStep,
416 /// `wait fork;` (LRM 9.6.1, SC-125): resume once every child this process
417 /// forked has finished. Payload-free -- the scheduler tracks the children.
418 WaitFork,
419 /// Classes plan Task 13 (LRM 9.7): `p.await();` / `p.suspend();` as a
420 /// statement. The call itself queues a process request; this suspend
421 /// yields so the scheduler applies it before the next statement runs
422 /// (`Suspend::ByRequest`). Payload-free.
423 ByRequest,
424}
425
426/// Task 7 (PR-116, XC-044): a fieldless, `'static`-safe mirror of
427/// [`SuspendPoint`]'s discriminants. `SuspendPoint<'d>` itself cannot be
428/// named `&'static [SuspendPoint]` -- every payload-carrying variant embeds
429/// a borrowed `sv_lang::Expression<'d>`, which is only ever alive for the
430/// lifetime of a real, already-compiled `Design`, so no `'static` instance
431/// of it can exist independent of one. This type is exactly the
432/// "which shape" tag [`suspending_statement_kinds`]'s two callers (the
433/// frame-slot round-trip matrix in `sv-sim-sim`'s own tests and the JIT+run
434/// matrix in `sv-sim-codegen`'s tests) both need, without requiring either
435/// to fabricate a borrowed `Expression` from nothing.
436#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
437pub enum SuspendPointKind {
438 Delay,
439 Edge,
440 EdgeList,
441 Level,
442 CycleDelay,
443 /// Task 6 (CG-081): `ImplicitEventControl` (`@*`/`@(*)`).
444 ImplicitComb,
445 /// Task 6 (CG-081): `OneStepDelayControl` (`#0`).
446 OneStep,
447 /// `wait fork;` (SC-125).
448 WaitFork,
449 /// `process::await()` / `process::suspend()` (Classes plan Task 13).
450 ByRequest,
451}
452
453impl<'d> SuspendPoint<'d> {
454 /// Exhaustive, no-wildcard match (ARCHITECTURE §7 item 9): if a future
455 /// task adds a new `SuspendPoint` variant without extending
456 /// [`SuspendPointKind`] and this match, the crate fails to COMPILE, not
457 /// merely under-tests -- a stronger guarantee than
458 /// [`suspending_statement_kinds`]'s own length alone could give.
459 pub fn kind(&self) -> SuspendPointKind {
460 match self {
461 SuspendPoint::Delay(..) => SuspendPointKind::Delay,
462 SuspendPoint::Edge { .. } => SuspendPointKind::Edge,
463 SuspendPoint::EdgeList(..) => SuspendPointKind::EdgeList,
464 SuspendPoint::Level(_) => SuspendPointKind::Level,
465 SuspendPoint::CycleDelay { .. } => SuspendPointKind::CycleDelay,
466 SuspendPoint::ImplicitComb(_) => SuspendPointKind::ImplicitComb,
467 SuspendPoint::OneStep => SuspendPointKind::OneStep,
468 SuspendPoint::WaitFork => SuspendPointKind::WaitFork,
469 SuspendPoint::ByRequest => SuspendPointKind::ByRequest,
470 }
471 }
472}
473
474/// Task 7 (PR-116, XC-044): every `SuspendPoint` kind this file's CFG
475/// builder currently gives a real shape to (i.e. every variant
476/// [`SuspendPoint`] itself has -- see that enum's own doc comment for the
477/// still-open `ImplicitEvent`/`RepeatedEvent`/`Delay3`/`OneStepDelay`/
478/// `BlockEventList` gap PR-069's extension policy keeps out of the enum
479/// entirely). One definition, promoted from a test-only helper to real
480/// production code specifically so both `sv-sim-sim`'s own meta-test and
481/// `sv-sim-codegen`'s JIT+run matrix enumerate the identical list, never two
482/// independently-maintained copies that could silently drift apart (this
483/// project's own "duplicate pure-function eval diverges" lesson).
484///
485/// **Not every kind in this list is yet resolvable by
486/// `sv-sim-codegen::process_lower::resolve_suspend_point`** --
487/// `EdgeList`/`CycleDelay` are real CFG shapes (this file's own Task 1) but
488/// still a named, codegen-side `LoweringError` ("this SuspendPoint kind has
489/// no codegen-side resolution yet"), a pre-existing, documented scope
490/// boundary from that same task, not something this row's own file list
491/// (`crates/sv-sim-sim/tests/frame_roundtrip_matrix.rs`, `crates/
492/// sv-sim-codegen/tests/frame_roundtrip_matrix.rs`, this file) can close
493/// without expanding into `sv-sim-codegen::process_lower`'s own `Files`
494/// scope. `sv-sim-codegen`'s own round-trip matrix asserts the SPECIFIC,
495/// named rejection for these two kinds (never silently skipping them), so
496/// the gate still fails loudly in both directions: if a kind that should
497/// round-trip stops doing so, or if a kind that should still be rejected
498/// silently starts "succeeding" with an unreviewed codegen change.
499pub fn suspending_statement_kinds() -> &'static [SuspendPointKind] {
500 &[
501 SuspendPointKind::Delay,
502 SuspendPointKind::Edge,
503 SuspendPointKind::EdgeList,
504 SuspendPointKind::Level,
505 SuspendPointKind::CycleDelay,
506 SuspendPointKind::ImplicitComb,
507 SuspendPointKind::OneStep,
508 SuspendPointKind::WaitFork,
509 SuspendPointKind::ByRequest,
510 ]
511}
512
513/// A `BasicBlock`'s single exit (PR-008, extended by Task 1).
514#[derive(Debug, Clone)]
515pub enum Terminator<'d> {
516 /// Falls straight into `BlockId` with no branch/suspend (e.g. the merge
517 /// point after an `if`/`else`).
518 Fallthrough(BlockId),
519 /// Goes to `arms[i]` when `on` evaluates to `i`, else to `default_blk`
520 /// -- reporting an `$error` at source offset `report` first, when set
521 /// (a `wait_order` with no `else`).
522 Switch {
523 on: SwitchOn<'d>,
524 arms: Vec<BlockId>,
525 default_blk: BlockId,
526 report: Option<sv_lang::SourceLoc>,
527 },
528 /// `if`/`case` (PR-044 -- expanded to a br-chain at the `Terminator`
529 /// level; the ternary operator `?:` never appears here, only inside a
530 /// straight-line expression within a [`BasicBlock`]'s `stmts`).
531 Branch {
532 cond: Expression<'d>,
533 then_blk: BlockId,
534 else_blk: BlockId,
535 },
536 /// An immediate assertion (LRM 16.3) whose pass or fail statement waits,
537 /// so the statement cannot stay a straight-line leaf. Goes to `pass_blk`
538 /// (`fail_blk`) when the expression is true (not true) and the assertion
539 /// is checking with that action on (20.11), else to `done_blk`; the
540 /// default `$error` of an assert/assume with no fail statement is
541 /// reported inline before `done_blk`.
542 Assertion {
543 stmt: Statement<'d>,
544 pass_blk: BlockId,
545 fail_blk: BlockId,
546 done_blk: BlockId,
547 },
548 Suspend {
549 point: SuspendPoint<'d>,
550 resume_blk: BlockId,
551 },
552 /// PR-048: `loop_back` is `None` for `initial`/`final` procedures and
553 /// `Some(entry)` for `always`/`always_ff`/`always_comb`/`always_latch`
554 /// procedures -- see [`ProcessLoopKind`].
555 End { loop_back: Option<BlockId> },
556 /// PR-045 (Task 1): `for`/`while`/`do-while`/`forever`/`foreach`.
557 /// `header`'s own terminator (a plain `Branch` for a real per-iteration
558 /// condition, or `Fallthrough` when there is none -- `forever`) carries
559 /// the actual per-iteration decision, and `body`'s own terminator closes
560 /// the back edge to `header`. `entry` is the block `cur` (the one this
561 /// `Terminator::Loop` closes) actually transfers control to on first
562 /// entry -- `header` for `while`/`for`/`forever`/`foreach` (the
563 /// condition, if any, is checked before the first iteration), but
564 /// `body` for `do-while` (the body always runs once before any check) --
565 /// a deviation from `docs/design/process.md` §2's literal 3-field
566 /// `Loop{header,body,exit}` (recorded in this task's deviations): a
567 /// `do-while`'s real entry edge genuinely cannot be recovered from
568 /// `header`/`body`/`exit` alone once `header` is required (as this
569 /// task's own tests require) to remain a distinct, inspectable
570 /// post-body recheck block rather than being aliased to `body`. Codegen
571 /// may fully unroll a static-bound `for`, but the CFG representation
572 /// itself always has a real `Loop` terminator so the liveness/segment-
573 /// lowering passes never special-case "was this unrolled" (this file's
574 /// own `lower_terminator` maps it losslessly to
575 /// `SegTerminator::Goto(entry)`, since whichever real block `entry`
576 /// names already carries its own correct terminator). `foreach`'s
577 /// per-dimension bound is a `ConstantRange`, not an `Expression` slang
578 /// exposes anywhere on the statement -- see `walk_foreach_loop`'s own
579 /// doc comment for why that one kind's `body` is a chain of unrolled
580 /// copies rather than a real back edge (a second, separately-recorded
581 /// deviation).
582 Loop {
583 header: BlockId,
584 body: BlockId,
585 exit: BlockId,
586 entry: BlockId,
587 },
588 /// `fork ... join`/`join_any`/`join_none` (Task 1, PR-069/docs/design/
589 /// process.md §7): construction only, never scheduled. Each
590 /// `branches[i]` is a full nested [`ProcessCfg`] (a fork branch can
591 /// contain its own suspends). See [`contains_unschedulable_fork_join`]
592 /// -- a real caller must check this before calling [`lower_to_segments`]
593 /// on a CFG that might contain one.
594 ForkJoin {
595 kind: StatementBlockKind,
596 branches: Vec<ProcessCfg<'d>>,
597 join_blk: BlockId,
598 },
599 /// R-run Task 8 (PR-045/PR-006): a `repeat(N) body;` loop's `header`
600 /// terminator, when `body` contains a suspend point (see
601 /// [`RepeatCounterId`]'s own doc comment for why this is a distinct
602 /// shape from `Branch`, never a fabricated `cond: Expression`). Mirrors
603 /// `while`'s own `header`/`Branch` role exactly -- `then_blk` is the
604 /// loop body, `else_blk` is the loop's exit -- except the re-checked
605 /// condition is `counter != 0` against this loop's own compiler-
606 /// synthesized [`RepeatCounterId`] slot, not a re-evaluated user
607 /// expression.
608 RepeatCheck {
609 counter: RepeatCounterId,
610 then_blk: BlockId,
611 else_blk: BlockId,
612 },
613 /// A `case`/`casez`/`casex`/`case ... inside` statement whose arms
614 /// cannot stay one straight-line leaf (an arm suspends, or `break`s /
615 /// `continue`s an enclosing process loop, or `return`s from an inlined
616 /// task): `stmt` supplies the selector and labels, matched in item
617 /// order, `arms[i]` is `stmt.case_items()[i]`'s block and `default_blk`
618 /// runs when no item matches. Also a `unique`/`unique0`/`priority` `if`
619 /// series: `stmt` is its first `if` ([`if_chain`]), `arms[i]` the
620 /// then-statement of the i-th condition and `default_blk` the final `else`,
621 /// so the dispatch can report a violation where the series executes.
622 /// Also an `if` with a pattern-matching or `&&&`-chained predicate (one
623 /// arm, [`is_clause_if`]) and a `case ... matches` (`arms[i]` is
624 /// `pattern_case_items()[i]`'s block, `default_blk` its `default`).
625 Case {
626 stmt: Statement<'d>,
627 arms: Vec<BlockId>,
628 default_blk: BlockId,
629 },
630}
631
632/// One argument transfer of a task call inlined into the process CFG
633/// (`CfgBuilder::inline_task_call`): a task whose body can suspend (`@`, `#`,
634/// `wait`, ...) cannot be an ordinary compiled function, so its body is
635/// walked in place and these run at the start of the inlined region's
636/// blocks. Every symbol named here is one of `ProcessCfg::inlined_locals`.
637#[derive(Debug, Clone)]
638pub enum ArgBinding<'d> {
639 /// `formal = actual`: an input (or the copy-in of an inout) formal.
640 In {
641 formal: Symbol<'d>,
642 actual: Expression<'d>,
643 },
644 /// `var` = its declaration initializer, or its type's default: an
645 /// `output` formal or one of the task's own locals, at task entry.
646 Init { var: Symbol<'d> },
647 /// `actual = formal`: an output/inout formal's copy-back after the body
648 /// (`actual` is the lvalue expression).
649 Out {
650 actual: Expression<'d>,
651 formal: Symbol<'d>,
652 },
653 /// `var = value`: a `foreach` loop variable's starting value.
654 Const { var: Symbol<'d>, value: i64 },
655 /// `var` = the current object: the `this` of a class-method task inlined
656 /// for an implicit-`this` call (`run()` inside another method), bound in
657 /// the caller's context. An explicit receiver (`obj.run()`) binds through
658 /// [`ArgBinding::In`] instead.
659 ThisOf { var: Symbol<'d> },
660 /// An inlined task's input formal or entry-initialized local ending
661 /// with the call (an automatic variable ends with its scope, LRM 6.21):
662 /// whatever class / collection handles it holds are released (LRM
663 /// 13.5.1). Runs at the call's exit block, so `return` and `disable`
664 /// release too.
665 Release { var: Symbol<'d> },
666 /// `var += by`: a `foreach` loop variable's per-iteration step.
667 Step { var: Symbol<'d>, by: i64 },
668 /// `void'(arr.first(var))`, or with `next` `void'(arr.next(var))`: a
669 /// `foreach` over the associative array `arr` walking its keys.
670 ///
671 /// For an associative array nested in a collection (`int m[int][string]`,
672 /// `int q[$][int]`), `path` holds the enclosing `foreach` levels that
673 /// select the array from `arr` (as [`RepeatCount::NestedSize`]); empty
674 /// for `arr` itself.
675 AssocKey {
676 var: Symbol<'d>,
677 arr: Expression<'d>,
678 path: Vec<NestStep<'d>>,
679 next: bool,
680 },
681 /// `lhs = #d rhs;` and the other intra-assignment timing controls (LRM
682 /// 1800-2023 §10.4.1): the right-hand side is evaluated before the wait
683 /// into the compiler temporary `temp` (a `FrameSlotSource::Temporary`
684 /// frame slot, sized by [`ProcessCfg::captures`]`[temp]`)...
685 Capture { temp: u32, value: Expression<'d> },
686 /// ...and assigned to its target after it.
687 ///
688 /// With `nba` (`lhs <= @(e) rhs;`, LRM 1800-2023 §10.4.2) the assignment
689 /// is a nonblocking one, staged for the NBA region: it runs in the
690 /// synthetic fork branch [`CfgBuilder::walk_nba_event`] spawns.
691 AssignCaptured {
692 temp: u32,
693 target: Expression<'d>,
694 nba: bool,
695 },
696 /// `formal = temp`: a recursive task's input formal, copied from the
697 /// call's evaluated argument ([`InheritedSlot::Temp`]) as the forked
698 /// activation starts ([`CfgBuilder::inline_recursive_task`]).
699 FromCaptured { temp: u32, formal: Symbol<'d> },
700 /// `temp = formal`: a recursive task's `output`/`inout` formal, parked in
701 /// a compiler temporary as the activation ends so the spawner can copy
702 /// it back ([`InheritedSlot::Return`]).
703 ToCaptured { temp: u32, formal: Symbol<'d> },
704 /// Marks capture temporary `temp` as the value of the inlined call `call`
705 /// of a function that spawns processes: the call's expression reads the
706 /// temporary (keyed by [`call_result_key`]) instead of the function's
707 /// shared result variable, which a second call of the same function in
708 /// the statement, or another override of a virtual method, overwrites.
709 /// A no-op at run time; [`ArgBinding::ToCaptured`] parks the value.
710 CallResult { temp: u32, call: Expression<'d> },
711 /// The resume of a mixed `@(class_event or signal ...)` wait (LRM 1800-2023
712 /// 9.4.2.1, 15.5): the process leaves the class-event queues it is still
713 /// queued on (the signal side woke it), so a later trigger of one of those
714 /// events cannot wake a wait it is no longer in.
715 ClassEventLeave,
716 /// `temp` = the caller's current object: the receiver slot of a recursive
717 /// class-method activation called with an implicit `this` (LRM 1800-2023
718 /// 8.11, 13.4.1); an explicit receiver is an ordinary
719 /// [`ArgBinding::Capture`].
720 CaptureThis { temp: u32 },
721 /// A `ref` formal bound to element `actual = c[i]` of a queue or dynamic
722 /// array (LRM 1800-2023 13.5.2, 7.10.3): opens a runtime reference to the
723 /// element into the 64-bit temporary `temp` (the collection's tracking
724 /// outdates it on delete / pop / clear / resize), then `formal` = the
725 /// element.
726 RefOpen {
727 temp: u32,
728 formal: Symbol<'d>,
729 actual: Expression<'d>,
730 },
731 /// The copy-out of [`ArgBinding::RefOpen`]: `actual` = `formal` unless
732 /// the reference `temp` was outdated meanwhile.
733 RefClose {
734 temp: u32,
735 formal: Symbol<'d>,
736 actual: Expression<'d>,
737 },
738}
739
740/// A frame slot a spawned branch copies by value from its parent
741/// ([`ProcessCfg::inherited`]).
742#[derive(Debug, Clone, Copy, PartialEq, Eq)]
743pub enum InheritedSlot {
744 /// Capture temporary `parent` of the forking process -> `child` of the branch.
745 Temp { parent: u32, child: u32 },
746 /// Repeat counter `parent` -> `child`.
747 Counter {
748 parent: RepeatCounterId,
749 child: RepeatCounterId,
750 },
751 /// The other way round, when the branch is done: its capture temporary
752 /// `child` -> temporary `parent` of the process that forked it (a
753 /// recursive task's `output`/`inout` formals, LRM 1800-2023 13.5).
754 Return { parent: u32, child: u32 },
755}
756
757/// What a [`Terminator::Switch`] selects on.
758#[derive(Clone)]
759pub enum SwitchOn<'d> {
760 /// The index of the wait term that woke the process at the suspend just
761 /// before (`sv_rt_fired_term`): `wait_order`.
762 FiredTerm,
763 /// A virtual interface's instance id minus one (`netlist::vif`): an
764 /// interface task called through it.
765 VifId(Expression<'d>),
766 /// A `randcase`'s item, picked at random by the items' weights (none
767 /// when every weight is 0).
768 RandCase(Statement<'d>),
769 /// The arm whose class template ids hold the dynamic class of the object
770 /// `receiver` names (the current object when `None`): a virtual class
771 /// task that can suspend, one inlined override per arm (LRM 8.20).
772 ObjClass {
773 receiver: Option<Expression<'d>>,
774 ids: Vec<Vec<u32>>,
775 },
776 /// No arm is ever taken: the switch only reports its error (`what` is the
777 /// message) and continues at the default -- a call to an `extern forkjoin`
778 /// task no module implements (LRM 25.7.5).
779 NoTarget { what: String },
780 /// The arm of the named block (`labels[i]`, innermost first) that the
781 /// subroutine call just before disabled with `disable <block>;` from
782 /// inside a function (LRM 9.6.2, 35.9), else the default
783 /// (`sv_rt_dpi_fn_disable_index`).
784 FnDisable { labels: Vec<String> },
785}
786
787impl std::fmt::Debug for SwitchOn<'_> {
788 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
789 match self {
790 SwitchOn::NoTarget { what } => write!(f, "NoTarget({what})"),
791 SwitchOn::FnDisable { labels } => write!(f, "FnDisable({labels:?})"),
792 SwitchOn::FiredTerm => write!(f, "FiredTerm"),
793 SwitchOn::VifId(e) => write!(f, "VifId({:?})", e.kind()),
794 SwitchOn::RandCase(_) => write!(f, "RandCase"),
795 SwitchOn::ObjClass { ids, .. } => write!(f, "ObjClass({ids:?})"),
796 }
797 }
798}
799
800impl<'d> SwitchOn<'d> {
801 /// Every expression the selection reads.
802 pub fn reads(&self) -> Vec<Expression<'d>> {
803 match self {
804 SwitchOn::FiredTerm | SwitchOn::NoTarget { .. } | SwitchOn::FnDisable { .. } => {
805 Vec::new()
806 }
807 SwitchOn::VifId(e) => vec![*e],
808 SwitchOn::RandCase(s) => s.randcase_items().iter().map(|i| i.expr()).collect(),
809 SwitchOn::ObjClass { receiver, .. } => receiver.iter().copied().collect(),
810 }
811 }
812}
813
814/// One enclosing `foreach` dimension on the way to a nested collection
815/// ([`RepeatCount::NestedSize`]): its index variable and, for a fixed-size
816/// dimension, its `(left, right)` bounds (`None`: a queue / dynamic array
817/// dimension, indexed from 0).
818#[derive(Debug, Clone, Copy)]
819pub struct NestStep<'d> {
820 pub var: Symbol<'d>,
821 pub fixed: Option<(i32, i32)>,
822 /// The dimension is an associative array: `var` holds a key, not a
823 /// position.
824 pub assoc: bool,
825}
826
827/// A counted loop's iteration count: a `repeat (N)` loop's count
828/// expression, a `foreach` loop's static element count, or the number of
829/// elements a queue or dynamic array has when its `foreach` starts.
830#[derive(Debug, Clone)]
831pub enum RepeatCount<'d> {
832 Expr(Expression<'d>),
833 Const(u64),
834 /// The size of this queue / dynamic array expression.
835 Size(Expression<'d>),
836 /// The size of the queue / dynamic array reached from `arr` by the
837 /// enclosing `foreach` indices `path` (`a[i][j]` of `foreach (a[i, j,
838 /// k])` over `int a[2][3][]`), read when the loop is entered.
839 NestedSize {
840 arr: Expression<'d>,
841 path: Vec<NestStep<'d>>,
842 },
843 /// A count the forking process already evaluated: the counter slot is
844 /// copied in at the spawn ([`InheritedSlot::Counter`]), never
845 /// initialized here. `0` is the count expression, which sizes the slot.
846 Inherited(Expression<'d>),
847}
848
849impl<'d> RepeatCount<'d> {
850 /// The expression the count is computed from, if the count is not a
851 /// constant (for `Size`, the collection).
852 pub fn expr(&self) -> Option<Expression<'d>> {
853 match self {
854 RepeatCount::Expr(e) | RepeatCount::Size(e) => Some(*e),
855 RepeatCount::NestedSize { arr, .. } => Some(*arr),
856 RepeatCount::Const(_) | RepeatCount::Inherited(_) => None,
857 }
858 }
859}
860
861/// A straight-line run of non-suspending statements ending in exactly one
862/// [`Terminator`] (PR-008).
863#[derive(Debug, Clone)]
864pub struct BasicBlock<'d> {
865 /// Inlined-task argument transfers, run before `stmts` (see
866 /// [`ArgBinding`]).
867 pub bindings: Vec<ArgBinding<'d>>,
868 /// The `this` of the innermost class-method task inlined at this block
869 /// (its body's blocks; its binding and continuation blocks belong to the
870 /// caller): what an implicit-`this` member access or method call in the
871 /// block resolves against when the process itself is not a method.
872 pub this_var: Option<Symbol<'d>>,
873 /// The `ref` formals of the tasks inlined at this block that name a
874 /// static variable or net of their call (LRM 13.5.2), with that storage:
875 /// the formal reads and writes the actual itself in this block, so one
876 /// process may bind the same formal to different actuals per call.
877 pub ref_aliases: Vec<(Symbol<'d>, Symbol<'d>)>,
878 pub stmts: Vec<Statement<'d>>,
879 /// VPI `cbStmt` probe points (IEEE 1800-2023 38.36.1): `(i, s)` means the
880 /// statement `s` is about to execute and the probe runs just before
881 /// `stmts[i]` (`i == stmts.len()` is after the last one, still before
882 /// `step_exprs` and the terminator). Only statements this CFG decomposes
883 /// (or keeps as a leaf, which codegen then does not probe again) appear
884 /// here; codegen emits a probe for them only when a VPI library is loaded.
885 pub probes: Vec<(usize, Statement<'d>)>,
886 /// VPI `cbDisable` probe points (38.36.1): `(i, s)` means the `disable`
887 /// statement `s`, which this CFG turned into a jump out of the named block
888 /// it names (so it is not in `stmts`), has taken effect; the probe runs
889 /// where `probes` at index `i` do, after them.
890 pub disable_probes: Vec<(usize, Statement<'d>)>,
891 /// VPI frame probe points (37.43) of the blocking tasks inlined into this
892 /// CFG: they run where `probes` at the same index do, after them (and
893 /// after any `disable_probes`).
894 pub frame_probes: Vec<(usize, FrameProbe<'d>)>,
895 /// Covergroup block events (LRM 19.3 `@@(begin t)` / `@@(end t)`) of the
896 /// blocking tasks inlined into this CFG: `(i, task, is_end)` signals the
897 /// begin (or end) of `task`'s activation where `probes` at index `i` run.
898 /// Codegen emits only those whose task a covergroup names.
899 pub block_events: Vec<(usize, Symbol<'d>, bool)>,
900 /// `(i, subroutine)`: `stmts[i]` was written in the body of the inlined `subroutine`, whose
901 /// time unit its delays (`q <= #d v`) are in (LRM 3.14.2.1). Absent for the process's own.
902 pub stmt_scopes: Vec<(usize, Symbol<'d>)>,
903 /// A `for` loop's per-iteration step expressions (e.g. `i++`), run after
904 /// `stmts` and before `terminator` fires (Task 1, `walk_for_loop`).
905 /// Empty for every block that is not a `for` loop's step block. A
906 /// separate field, not folded into `stmts: Vec<Statement<'d>>`, because
907 /// `Statement::for_loop_steps()` returns bare `Expression<'d>`s (`i++`
908 /// is an expression in this grammar position, never a `Statement` --
909 /// confirmed against the pinned `sv-lang`=0.1.9 binding, which has no
910 /// expression-to-statement wrapper) and this crate never fabricates AST
911 /// nodes. Every slot this task's `for` fixture writes through
912 /// (`i` itself) is a `FrameSlotSource::DeclaredVar` permanent slot
913 /// (`lower_to_segments`'s own doc comment), so omitting these from
914 /// `liveness::compute_liveness`'s backward read/write walk does not
915 /// under-approximate any frame-slot's live range -- codegen consumption
916 /// of this field is future work, tracked in this task's deviations, not
917 /// a silent drop (the expressions are still here, in the CFG, for
918 /// whichever pass reads `step_exprs` next).
919 pub step_exprs: Vec<Expression<'d>>,
920 /// R-run Task 8: `Some((counter, count_expr))` iff this block is a
921 /// suspend-containing `repeat(N)` loop's own entry block -- `count_expr`
922 /// (`RepeatLoop::condition()`, per LRM the loop's iteration COUNT, not a
923 /// boolean) is evaluated exactly ONCE, run after `stmts` and before
924 /// `terminator` fires (mirrors [`Self::step_exprs`]'s own "run after
925 /// stmts, before terminator" placement), its result (clamped to
926 /// non-negative) becoming `counter`'s starting value. `None` for every
927 /// other block, including a suspend-free `repeat` (which stays the
928 /// pre-existing opaque leaf `stmt_shared::lower_repeat_loop` already
929 /// handles -- see [`RepeatCounterId`]'s own doc comment).
930 pub repeat_init: Option<(RepeatCounterId, RepeatCount<'d>)>,
931 /// R-run Task 8: `Some(counter)` iff this block is a suspend-containing
932 /// `repeat(N)` loop's body's own back-edge block -- decrements
933 /// `counter` by exactly one, run after `stmts`/`step_exprs` and before
934 /// `terminator` fires. `None` for every other block.
935 pub repeat_decrement: Option<RepeatCounterId>,
936 /// `true` iff this block is the one an `expect` statement's wait resumes
937 /// into (LRM 1800-2023 §16.17): its first statement is that `expect` again,
938 /// whose resume half (the pass/`else` action, selected by the outcome the
939 /// process was woken with) codegen lowers there.
940 pub expect_resume: bool,
941 pub terminator: Terminator<'d>,
942}
943
944/// PR-004, PR-008: the CFG built by [`build_cfg`] -- one LLVM function per
945/// process (Task S3c) will eventually `switch` on `resume_pc` with one case
946/// per segment derived from `blocks` below.
947#[derive(Debug, Clone)]
948pub struct ProcessCfg<'d> {
949 pub blocks: Vec<BasicBlock<'d>>,
950 pub entry: BlockId,
951 /// The process body's original root `Statement` (Task S3b deviation,
952 /// not in Task S3a's original field list): `process::liveness::
953 /// compute_liveness`'s declared-local discovery needs to walk the
954 /// *pre-flattening* statement tree looking for `Block` statements'
955 /// `block_symbol()` scopes -- `blocks`/`stmts` above only ever retain
956 /// the leaf statements `CfgBuilder::walk_seq` flattened into each
957 /// `BasicBlock`, which discards every intervening `begin`/`end` `Block`
958 /// statement node (and, with it, that block's `StatementBlockSymbol` --
959 /// the ONLY place `sv-lang`=0.1.9's pinned C API exposes a declared
960 /// local's `Symbol`; there is no per-`VariableDeclaration`-statement
961 /// symbol accessor at all, confirmed against the real binding). Keeping
962 /// the untouched root here, alongside the flattened `blocks`, is the
963 /// minimal way to make both queries possible without re-walking from
964 /// the caller.
965 pub body: Statement<'d>,
966 /// The formals and locals of every task inlined into this CFG
967 /// ([`ArgBinding`]) -- declared locals of the process for frame-layout
968 /// purposes, though no `Block` under `body` declares them.
969 pub inlined_locals: Vec<Symbol<'d>>,
970 /// The target of each intra-assignment-timed assignment's captured
971 /// right-hand side ([`ArgBinding::Capture`]), indexed by `temp`: its
972 /// type sizes the temporary's frame slot.
973 pub captures: Vec<Expression<'d>>,
974 /// For a fork branch: the variables of the forking activation it uses
975 /// (LRM 1800-2023 §9.3.2, §6.21) -- shared with it, not copied: their
976 /// frame slots, here and in the parent, hold a pointer to one cell.
977 pub captured: Vec<Symbol<'d>>,
978 /// `(formal, actual)`: a `ref` formal of an inlined task that every call
979 /// in this process binds to the same static variable or net (LRM
980 /// 1800-2023 §13.5.2). The formal names the actual's storage -- it has no
981 /// frame slot and no copy-in/copy-out -- so a wait on it (`@(posedge
982 /// clk)` with `ref logic clk`) sees the actual change.
983 pub ref_bindings: Vec<(Symbol<'d>, Symbol<'d>)>,
984 /// The name(s) a `disable <label>;` ANYWHERE (this process, a sibling
985 /// branch, an ancestor fork, or a genuinely different process) may
986 /// single this CFG's own live process out by (LRM 9.6.2): the
987 /// enclosing `fork : NAME`'s name, if this CFG is one of its branches,
988 /// and/or this CFG's own named outermost block (`peer_block(body)`),
989 /// if it has one. Empty when nothing could ever name this process this
990 /// way. `sv-sim-codegen::process_lower`'s fork/join lowering copies this
991 /// into each branch's own [`super::super::sched::SpawnSpec::
992 /// disable_name`].
993 pub disable_names: Vec<String>,
994 /// Every name [`Self::disable_names`] of a fork branch this CFG's own
995 /// walk resolved a `disable <label>;` against WITHOUT a same-process
996 /// lexical match (not an enclosing [`BasicBlock`], not this CFG's own
997 /// `fork_name`, not a procedural-assertion label): the kept `disable`
998 /// statement's codegen (`sv-sim-codegen::process_lower`) reads this to
999 /// tell such a statement apart from the ones that resolve to the OLDER,
1000 /// same-fork-only `sv_rt_disable_fork_block` path -- see
1001 /// [`CfgBuilder::fork_branch_names`]'s own doc comment for how it
1002 /// accumulates (parent-to-branch, branch-to-branch, and forward past
1003 /// the fork statement that spawned them).
1004 pub fork_branch_names: Vec<String>,
1005 /// `(resume, label, exit)`: while the process is suspended and will resume
1006 /// at block `resume`, it is inside the named block or task activation
1007 /// `label`, which a `disable label;` of another process ends by resuming
1008 /// the process at block `exit` instead (LRM 9.6.2). Only labels some
1009 /// `disable` of the design names ([`is_disabled_somewhere`]) are listed.
1010 /// A segment index is a valid resume point of the compiled kernel.
1011 pub disable_scopes: Vec<(BlockId, String, BlockId)>,
1012 /// The labels of the `disable` statements this CFG keeps as statements
1013 /// because they must also reach the processes other than this one that
1014 /// are inside the named block or task (`sv_rt_disable_named`), with
1015 /// whether the statement is the whole effect of the disable (an
1016 /// unresolved target: no jump follows, so codegen emits the VPI
1017 /// `cbDisable` probe itself).
1018 pub disable_broadcast: Vec<(String, bool)>,
1019 /// For a synthetic fork branch ([`CfgBuilder::walk_nba_event`]): the
1020 /// frame slots of the forking process copied BY VALUE into this
1021 /// branch's own at the spawn (LRM 1800-2023 §10.4.2: the right-hand
1022 /// side and the repeat count are evaluated when the assignment is
1023 /// reached, not when its event fires).
1024 pub inherited: Vec<InheritedSlot>,
1025 /// Per capture temporary (`captures`), the formal whose type sizes its
1026 /// frame slot instead of the captured expression's (a recursive task's
1027 /// call arguments, [`CfgBuilder::inline_recursive_task`]); entries past
1028 /// the end are `None`.
1029 pub capture_types: Vec<Option<Symbol<'d>>>,
1030 /// Whether this CFG is a recursive task's activation ([`Recursion`]).
1031 pub recursion: Recursion,
1032 /// Two fork branches carrying the same id were built from the same
1033 /// statement in the same inherited context ([`ForkBranchKey`]), so
1034 /// they are the same CFG and lower to the same kernel: the lowering
1035 /// compiles it once per process and points every spawn at that code
1036 /// (a UVM testbench otherwise cloned each branch once per path of
1037 /// nested forks leading to it, tens of GB of IR). `None` for a branch
1038 /// that is not shared.
1039 pub share_id: Option<u64>,
1040}
1041
1042/// A fork branch's part in a recursive task with timing controls (LRM
1043/// 1800-2023 §13.4, §13.4.1): every call is a `join` of one forked child
1044/// running the task body in a frame of its own.
1045#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1046pub enum Recursion {
1047 /// An ordinary CFG.
1048 None,
1049 /// The task's body: the branch the first call forks. Its kernel is
1050 /// compiled once and also serves every recursive call inside it.
1051 Root,
1052 /// A placeholder branch (no blocks of its own): a recursive call inside
1053 /// the enclosing [`Recursion::Root`], which spawns that root's kernel again.
1054 SelfSpawn,
1055 /// A placeholder branch: a call, inside one recursive activation, of the
1056 /// task of an enclosing activation (mutual recursion: `a` calls `b` calls
1057 /// `a`). It spawns that activation's kernel again; the number says how
1058 /// many [`Recursion::Root`] levels out it is (1 = the activation this one
1059 /// was forked from).
1060 AncestorSpawn(u32),
1061}
1062
1063/// Whether the process re-triggers from `entry` after finishing one pass
1064/// through its body (`always`/`always_ff`/`always_comb`/`always_latch`) or
1065/// runs exactly once (`initial`/`final`) -- PR-048's `Terminator::End{
1066/// loop_back}` distinction. Deliberately **not**
1067/// `sv-sim-codegen::module::SliceProcKind`: this crate must never depend on
1068/// `sv-sim-codegen` (see [`LoweringError`]'s doc comment for the same
1069/// reasoning); a caller that already has a `SliceProcKind` (module
1070/// discovery, Task S2/S7) maps it down to this coarser 2-value distinction
1071/// itself (`Initial` -> `OneShot`, `Always`/`AlwaysFF` -> `Looping`).
1072#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1073pub enum ProcessLoopKind {
1074 OneShot,
1075 Looping,
1076}
1077
1078/// PR-007: the single recursive-descent CFG builder entry point.
1079pub fn build_cfg<'d>(
1080 body: &Statement<'d>,
1081 loop_kind: ProcessLoopKind,
1082) -> Result<ProcessCfg<'d>, LoweringError> {
1083 build_cfg_in(
1084 body,
1085 loop_kind,
1086 None,
1087 None,
1088 None,
1089 None,
1090 Inherited::default(),
1091 None,
1092 )
1093}
1094
1095/// The CFG of a one-shot process that runs the body of `task` once, for a
1096/// task exported to C (`export "DPI-C" task`, LRM 1800-2023 35.7) whose body
1097/// consumes time (or stages a nonblocking assignment): C cannot run it as a
1098/// function, so each call from C spawns a process of this CFG and the
1099/// importing task blocks until it finishes (35.5.3).
1100///
1101/// The formals are ordinary locals of the process's frame, with NO entry
1102/// binding: the spawner stores the C arguments into their frame slots before
1103/// the process starts and reads the `output`/`inout` ones back when it ends
1104/// (the process's frame is what a copy-in/copy-out call has in a caller).
1105/// Other automatic locals get their declaration initialisers as usual.
1106/// `static` formals are module state, not frame slots; they stay out of
1107/// `inlined_locals`, exactly as a call inlined into a process treats them.
1108///
1109/// Fails by name for a task that calls itself (a recursive activation needs
1110/// the fork-per-call machinery of [`Recursion::Root`], whose argument
1111/// capture an export cannot supply).
1112pub fn build_export_task_cfg<'d>(
1113 task: &Symbol<'d>,
1114 global_clock: Option<InferredClockEvent<'d>>,
1115) -> Result<ProcessCfg<'d>, LoweringError> {
1116 let body = proc_body(task).ok_or_else(|| {
1117 LoweringError::malformed(format!("exported task `{}` has no body", task.name()))
1118 })?;
1119 let mut b = CfgBuilder::new();
1120 b.global_clock = global_clock;
1121 let entry = b.alloc_block();
1122 // Calling itself from its own body would inline without end; the
1123 // recursion machinery needs a caller's actuals to type its captures.
1124 b.rec_outer.push(task.uid());
1125 for f in task.subroutine_arguments() {
1126 if !crate::netlist::is_static_procedural_var(&f) {
1127 b.inlined_locals.push(f);
1128 }
1129 }
1130 for local in task_locals(task) {
1131 if crate::netlist::is_static_procedural_var(&local) {
1132 continue;
1133 }
1134 if !is_automatic_block_var(&local) {
1135 b.blocks[entry.0 as usize]
1136 .bindings
1137 .push(ArgBinding::Init { var: local });
1138 }
1139 b.inlined_locals.push(local);
1140 }
1141 for v in crate::patterns::pattern_vars(body.as_sem_node()) {
1142 if !b.inlined_locals.iter().any(|l| l.uid() == v.uid()) {
1143 b.inlined_locals.push(v);
1144 }
1145 }
1146 let after = b.alloc_block();
1147 b.target_depth.insert(after, 0);
1148 b.return_targets.push(after);
1149 let exit = b.walk_stmt(body, entry)?;
1150 b.close(exit, Terminator::Fallthrough(after));
1151 b.close(after, Terminator::End { loop_back: None });
1152 let inlined_locals = std::mem::take(&mut b.inlined_locals);
1153 let captures = std::mem::take(&mut b.captures);
1154 let capture_types = std::mem::take(&mut b.capture_types);
1155 let ref_bindings = b.take_ref_bindings();
1156 let disable_scopes = b.take_disable_scopes();
1157 let disable_broadcast = std::mem::take(&mut b.disable_broadcast);
1158 let fork_branch_names = std::mem::take(&mut b.fork_branch_names);
1159 Ok(ProcessCfg {
1160 blocks: b.finish(),
1161 entry,
1162 body,
1163 inlined_locals,
1164 captures,
1165 captured: Vec::new(),
1166 ref_bindings,
1167 disable_names: Vec::new(),
1168 fork_branch_names,
1169 disable_scopes,
1170 disable_broadcast,
1171 inherited: Vec::new(),
1172 capture_types,
1173 recursion: Recursion::None,
1174 share_id: None,
1175 })
1176}
1177
1178/// The one-shot process a task called from a sequence match item runs as
1179/// (LRM 1800-2023 16.11: "the subroutine call is scheduled in the Reactive
1180/// region ... as a thread of its own"). Its body is the call `call` (a user
1181/// task that can suspend, [`subroutine_can_suspend`]) inlined the way a call
1182/// statement of an ordinary process is: the formals are bound from the actual
1183/// expressions by [`ArgBinding::In`] in the entry block, so the process reads
1184/// its by-value actuals when it starts (the assertion kernel lowers each as the
1185/// value the engine sampled at the end point, `sva::proc::call_arg_key`) and
1186/// every call is its own activation with its own frame.
1187///
1188/// Fails by name for a task that calls itself (a recursive activation needs the
1189/// fork-per-call machinery of [`Recursion::Root`], whose argument capture a
1190/// spawned call cannot supply).
1191pub fn build_match_item_task_cfg<'d>(
1192 call: Expression<'d>,
1193) -> Result<ProcessCfg<'d>, LoweringError> {
1194 let task = call.call_subroutine().ok_or_else(|| {
1195 LoweringError::malformed("a match-item call of a task has no subroutine symbol")
1196 })?;
1197 let body = proc_body(&task)
1198 .ok_or_else(|| LoweringError::malformed(format!("task `{}` has no body", task.name())))?;
1199 let mut b = CfgBuilder::new();
1200 let entry = b.alloc_block();
1201 let exit = b.inline_task_call(call, task, entry)?;
1202 b.close(exit, Terminator::End { loop_back: None });
1203 let inlined_locals = std::mem::take(&mut b.inlined_locals);
1204 let captures = std::mem::take(&mut b.captures);
1205 let capture_types = std::mem::take(&mut b.capture_types);
1206 let ref_bindings = b.take_ref_bindings();
1207 let disable_scopes = b.take_disable_scopes();
1208 let disable_broadcast = std::mem::take(&mut b.disable_broadcast);
1209 let fork_branch_names = std::mem::take(&mut b.fork_branch_names);
1210 Ok(ProcessCfg {
1211 blocks: b.finish(),
1212 entry,
1213 body,
1214 inlined_locals,
1215 captures,
1216 captured: Vec::new(),
1217 ref_bindings,
1218 disable_names: Vec::new(),
1219 fork_branch_names,
1220 disable_scopes,
1221 disable_broadcast,
1222 inherited: Vec::new(),
1223 capture_types,
1224 recursion: Recursion::None,
1225 share_id: None,
1226 })
1227}
1228
1229/// The clocking event a checker's `$inferred_clock` (LRM 17.4) stands for: the
1230/// signal and edge of the default clocking of the scope that instantiates the
1231/// checker (14.12), which a checker procedure's `@clock` waits on when `clock`
1232/// kept its default. The third member is the event's `iff` condition
1233/// (`default clocking @(posedge clk iff en);`, 9.4.2.3), which gates every wait
1234/// on it; `None` for `@$global_clock`.
1235pub type InferredClockEvent<'d> = (Expression<'d>, EdgeKind, Option<Expression<'d>>);
1236
1237/// Whether `signal` (the expression of a signal event control) is a call of
1238/// `$global_clock` (14.14).
1239pub fn is_global_clock_call(signal: &Expression<'_>) -> bool {
1240 signal.kind() == ExpressionKind::Call
1241 && signal.is_system_call() == Some(true)
1242 && signal
1243 .system_subroutine()
1244 .is_some_and(|s| s.known_name_id() == sv_lang::KnownSystemName::GlobalClock)
1245}
1246
1247/// Whether `signal` (the expression of a signal event control) is a call of
1248/// `$inferred_clock`: a checker's `event clock = $inferred_clock` formal, used as
1249/// `@clock` by a procedure of an instance that did not bind it (17.4).
1250pub fn is_inferred_clock_call(signal: &Expression<'_>) -> bool {
1251 signal.kind() == ExpressionKind::Call
1252 && signal.is_system_call() == Some(true)
1253 && signal
1254 .system_subroutine()
1255 .is_some_and(|s| s.known_name_id() == sv_lang::KnownSystemName::InferredClock)
1256}
1257
1258/// The clocking block of a drive target rooted in a clock variable
1259/// (`cb.q`, `cb.q[3:0]`; LRM 1800-2023 14.16), else `None`.
1260fn drive_clocking_of<'d>(mut target: Expression<'d>) -> Option<Symbol<'d>> {
1261 while matches!(
1262 target.kind(),
1263 ExpressionKind::ElementSelect | ExpressionKind::RangeSelect
1264 ) {
1265 target = target.select_value()?;
1266 }
1267 let var = target.referenced_symbol()?;
1268 if var.kind() != SymbolKind::ClockVar {
1269 return None;
1270 }
1271 var.parent()
1272 .filter(|p| p.kind() == SymbolKind::ClockingBlock)
1273}
1274
1275/// The clock variable of a drive target rooted in a virtual interface member
1276/// access (`v.sb.c`, `v.sb.c[3:0]`), else `None`.
1277fn drive_via_of<'d>(mut target: Expression<'d>) -> Option<Expression<'d>> {
1278 while matches!(
1279 target.kind(),
1280 ExpressionKind::ElementSelect | ExpressionKind::RangeSelect
1281 ) {
1282 target = target.select_value()?;
1283 }
1284 (crate::netlist::vif::is_member_access(&target)).then_some(target)
1285}
1286
1287/// [`build_cfg`] for a process of an instance whose default clocking block
1288/// is `default_clocking` (what `##N` counts).
1289pub fn build_cfg_clocked<'d>(
1290 body: &Statement<'d>,
1291 loop_kind: ProcessLoopKind,
1292 default_clocking: Option<Symbol<'d>>,
1293) -> Result<ProcessCfg<'d>, LoweringError> {
1294 build_cfg_scoped(body, loop_kind, default_clocking, &[], None)
1295}
1296
1297/// [`build_cfg_clocked`] for a process that shares its instance with the
1298/// processes whose outermost named blocks are `peers`: a `disable` of one of
1299/// them is accepted when it cannot be inside the block (see [`PeerBlock`]).
1300/// `inferred_clock` is the event a checker procedure's `@clock` waits on when its
1301/// `clock` formal kept the default `$inferred_clock` (17.4); `None` for a process
1302/// that is not a checker's or whose instantiation context infers no clock.
1303pub fn build_cfg_scoped<'d>(
1304 body: &Statement<'d>,
1305 loop_kind: ProcessLoopKind,
1306 default_clocking: Option<Symbol<'d>>,
1307 peers: &[PeerBlock],
1308 inferred_clock: Option<InferredClockEvent<'d>>,
1309) -> Result<ProcessCfg<'d>, LoweringError> {
1310 build_cfg_in(
1311 body,
1312 loop_kind,
1313 None,
1314 default_clocking,
1315 Some(peers),
1316 inferred_clock,
1317 Inherited::default(),
1318 None,
1319 )
1320}
1321
1322/// [`build_cfg_scoped`] that can dispatch a virtual class task that suspends
1323/// over the class templates of `virtual_tasks`.
1324pub fn build_cfg_scoped_with_virtuals<'d>(
1325 body: &Statement<'d>,
1326 loop_kind: ProcessLoopKind,
1327 default_clocking: Option<Symbol<'d>>,
1328 peers: &[PeerBlock],
1329 inferred_clock: Option<InferredClockEvent<'d>>,
1330 global_clock: Option<InferredClockEvent<'d>>,
1331 virtual_tasks: Option<std::rc::Rc<VirtualTasks<'d>>>,
1332) -> Result<ProcessCfg<'d>, LoweringError> {
1333 build_cfg_in(
1334 body,
1335 loop_kind,
1336 None,
1337 default_clocking,
1338 Some(peers),
1339 inferred_clock,
1340 Inherited {
1341 global_clock,
1342 ..Inherited::default()
1343 },
1344 virtual_tasks,
1345 )
1346}
1347
1348/// The outermost named block of another process of the instance, which a
1349/// `disable` may target (LRM 9.6.2, 16.14.6.4).
1350#[derive(Clone, Debug, PartialEq, Eq)]
1351pub struct PeerBlock {
1352 pub name: String,
1353 /// The block contains a suspend point, so its process may be inside it. A
1354 /// `disable` then has to terminate the process's activity (9.6.2), which
1355 /// the scheduler cannot do; when the block cannot suspend, the process is
1356 /// never inside it while another runs, and the disable's only effect is the
1357 /// flush of the process's pending procedural assertion queue.
1358 pub can_suspend: bool,
1359 /// The block is the outermost block of a fork branch (its own process,
1360 /// tagged by name): any number of those may share a name, and `disable
1361 /// <name>;` ends every one (LRM 9.6.2).
1362 pub fork_branch: bool,
1363}
1364
1365/// The outermost scope of a process body (LRM 16.14.6.2): the block statement
1366/// the body is, looking through the leading timing controls of an `always`
1367/// (`always @(a or b) begin : b2 ... end`). `None` when the body is not a block.
1368pub fn outermost_block<'d>(body: &Statement<'d>) -> Option<Statement<'d>> {
1369 let mut cur = *body;
1370 loop {
1371 // Not a `match` over the kind: every other statement kind means "the body
1372 // is not a block", which is an answer, not an unimplemented case.
1373 let kind = cur.kind();
1374 if kind == StatementKind::Block {
1375 return Some(cur);
1376 }
1377 if kind == StatementKind::Timed {
1378 cur = cur.body()?;
1379 } else if kind == StatementKind::List {
1380 let list = live_statements(&cur);
1381 let [only] = list.as_slice() else {
1382 return None;
1383 };
1384 cur = *only;
1385 } else {
1386 return None;
1387 }
1388 }
1389}
1390
1391/// The named outermost block of the process body `body`, as another process
1392/// sees it ([`PeerBlock`]); `None` for an unnamed one.
1393pub fn peer_block<'d>(body: &Statement<'d>) -> Option<PeerBlock> {
1394 let block = outermost_block(body)?;
1395 let name = block_name(&block);
1396 if name.is_empty() {
1397 return None;
1398 }
1399 Some(PeerBlock {
1400 name,
1401 can_suspend: contains_suspend_trigger_in(
1402 block.as_sem_node(),
1403 &mut std::collections::HashSet::new(),
1404 true,
1405 false,
1406 ),
1407 fork_branch: false,
1408 })
1409}
1410
1411/// The hierarchical path of every named block (sequential or fork) in the
1412/// process body `body`, dotted from its outermost named block down
1413/// (`a`, `a.b`, `a.b.c`, `a.f.br1`): what a `disable s.a.b.c.inner;` from
1414/// another instance names, however deep (LRM 9.6.2, 23.6).
1415pub fn named_block_paths(body: &Statement<'_>) -> Vec<String> {
1416 let mut out = Vec::new();
1417 let mut stack: Vec<(SemNode<'_>, String)> = vec![(body.as_sem_node(), String::new())];
1418 while let Some((n, prefix)) = stack.pop() {
1419 let mut here = prefix.clone();
1420 if let Some(s) = n.as_statement()
1421 && s.kind() == StatementKind::Block
1422 {
1423 let name = block_name(&s);
1424 if !name.is_empty() {
1425 here = if prefix.is_empty() {
1426 name
1427 } else {
1428 format!("{prefix}.{name}")
1429 };
1430 out.push(here.clone());
1431 }
1432 }
1433 stack.extend(live_children(&n).into_iter().map(|c| (c, here.clone())));
1434 }
1435 out
1436}
1437
1438/// The named sequential blocks nested in the process body `body` below its
1439/// outermost scope, as another process sees them ([`PeerBlock`]): a `disable`
1440/// of one of them from another process ends the activity inside it (LRM
1441/// 9.6.2). A fork's branches are processes of their own and are not entered.
1442pub fn nested_peer_blocks<'d>(body: &Statement<'d>) -> Vec<PeerBlock> {
1443 let outermost = outermost_block(body).map(|b| b.source_range().start.offset);
1444 let mut out = Vec::new();
1445 let mut stack = vec![body.as_sem_node()];
1446 while let Some(n) = stack.pop() {
1447 if let Some(s) = n.as_statement()
1448 && s.kind() == StatementKind::Block
1449 {
1450 match s.block_kind() {
1451 Some(StatementBlockKind::Sequential) => {
1452 let name = block_name(&s);
1453 if !name.is_empty() && outermost != Some(s.source_range().start.offset) {
1454 out.push(PeerBlock {
1455 name,
1456 can_suspend: contains_suspend_trigger_in(
1457 n,
1458 &mut std::collections::HashSet::new(),
1459 true,
1460 false,
1461 ),
1462 fork_branch: false,
1463 });
1464 }
1465 }
1466 Some(
1467 StatementBlockKind::JoinAll
1468 | StatementBlockKind::JoinAny
1469 | StatementBlockKind::JoinNone,
1470 ) => {
1471 fork_branch_peers(n, &mut out);
1472 continue;
1473 }
1474 None => {}
1475 }
1476 }
1477 stack.extend(live_children(&n));
1478 }
1479 out
1480}
1481
1482/// The named outermost blocks of the branches of the fork `fork` and of every
1483/// fork nested in those branches: each branch is a process of its own tagged
1484/// with its block's name ([`ProcessCfg::disable_names`]), which `disable
1485/// <name>;` of another process ends (LRM 9.6.2). A branch's process may be
1486/// anywhere, so these always `can_suspend`. Named blocks nested deeper in a
1487/// branch are listed as ordinary nested peers.
1488fn fork_branch_peers(fork: SemNode<'_>, out: &mut Vec<PeerBlock>) {
1489 let mut branches = fork.children();
1490 if let [only] = branches.as_slice()
1491 && only
1492 .as_statement()
1493 .is_some_and(|s| s.kind() == StatementKind::List)
1494 {
1495 branches = only.children();
1496 }
1497 for b in branches {
1498 let Some(s) = b.as_statement() else { continue };
1499 if let Some(mut p) = peer_block(&s) {
1500 p.can_suspend = true;
1501 p.fork_branch = true;
1502 out.push(p);
1503 }
1504 // Blocks nested in the branch (and in forks nested in it) are blocks
1505 // of the branch's process: its kernel carries their disable scopes
1506 // (`ProcessCfg::disable_scopes`, `ForkKernel::disable_scopes`).
1507 out.extend(nested_peer_blocks(&s));
1508 }
1509}
1510
1511impl ProcessCfg<'_> {
1512 /// The labels of the disable scopes of every fork branch under this CFG
1513 /// (nested forks included): the blocks of the branch kernels a
1514 /// `disable` of another process can end (LRM 9.6.2).
1515 pub fn branch_disable_labels(&self) -> Vec<String> {
1516 let mut out = Vec::new();
1517 for b in &self.blocks {
1518 if let Terminator::ForkJoin { branches, .. } = &b.terminator {
1519 for br in branches {
1520 out.extend(br.disable_scopes.iter().map(|(_, l, _)| l.clone()));
1521 out.extend(br.branch_disable_labels());
1522 }
1523 }
1524 }
1525 out
1526 }
1527
1528 /// The `disable` broadcast targets of every fork branch under this CFG
1529 /// (nested forks included), as [`ProcessCfg::disable_broadcast`]: a
1530 /// `disable u1.blk;` inside a branch must resolve as one in the process
1531 /// itself does, or be refused (LRM 9.6.2).
1532 pub fn branch_disable_broadcast(&self) -> Vec<(String, bool)> {
1533 let mut out = Vec::new();
1534 for b in &self.blocks {
1535 if let Terminator::ForkJoin { branches, .. } = &b.terminator {
1536 for br in branches {
1537 out.extend(br.disable_broadcast.iter().cloned());
1538 out.extend(br.branch_disable_broadcast());
1539 }
1540 }
1541 }
1542 out
1543 }
1544}
1545
1546/// The label of every procedural concurrent assertion (not an `expect`, which
1547/// blocks) in the subtree of `body`, with the assertion statement: a labeled
1548/// statement is a block of that name holding it, which `disable <label>;`
1549/// names (16.14.6.4).
1550pub fn assertion_labels<'d>(body: &Statement<'d>) -> Vec<(String, Statement<'d>)> {
1551 let mut out = Vec::new();
1552 let mut stack = vec![body.as_sem_node()];
1553 while let Some(n) = stack.pop() {
1554 if let Some(s) = n.as_statement()
1555 && s.kind() == StatementKind::Block
1556 && let name = block_name(&s)
1557 && !name.is_empty()
1558 && let [only] = s.statements().as_slice()
1559 && only.kind() == StatementKind::ConcurrentAssertion
1560 && !is_expect(only)
1561 {
1562 out.push((name, *only));
1563 }
1564 stack.extend(live_children(&n));
1565 }
1566 out
1567}
1568
1569/// The label of every deferred immediate assertion (`L: assert #0 (c) ...;`,
1570/// `assert final`) in the subtree of `body`, with the assertion statement: a
1571/// labeled statement is a block of that name holding it, which `disable L;`
1572/// names (LRM 16.4.4: cancels that assertion's pending reports).
1573pub fn deferred_assertion_labels<'d>(body: &Statement<'d>) -> Vec<(String, Statement<'d>)> {
1574 let mut out = Vec::new();
1575 let mut stack = vec![body.as_sem_node()];
1576 while let Some(n) = stack.pop() {
1577 if let Some(s) = n.as_statement()
1578 && s.kind() == StatementKind::Block
1579 && let name = block_name(&s)
1580 && !name.is_empty()
1581 && let [only] = s.statements().as_slice()
1582 && only.kind() == StatementKind::ImmediateAssertion
1583 && only.is_deferred_assertion() == Some(true)
1584 {
1585 out.push((name, *only));
1586 }
1587 stack.extend(live_children(&n));
1588 }
1589 out
1590}
1591
1592/// The label of every plain (non-deferred) immediate assertion in the subtree
1593/// of `body`: a labeled statement is a block of that name holding it. A
1594/// `disable L;` outside that block (LRM 9.6.2) finds the block already
1595/// finished -- an immediate assertion's own block has no suspend point the
1596/// disable could catch from the same process.
1597pub fn immediate_assertion_labels(body: &Statement<'_>) -> Vec<String> {
1598 let mut out = Vec::new();
1599 let mut stack = vec![body.as_sem_node()];
1600 while let Some(n) = stack.pop() {
1601 if let Some(s) = n.as_statement()
1602 && s.kind() == StatementKind::Block
1603 && let name = block_name(&s)
1604 && !name.is_empty()
1605 && let [only] = s.statements().as_slice()
1606 && only.kind() == StatementKind::ImmediateAssertion
1607 && only.is_deferred_assertion() != Some(true)
1608 {
1609 out.push(name);
1610 }
1611 stack.extend(live_children(&n));
1612 }
1613 out
1614}
1615
1616/// Whether `stmt` is a labeled immediate assertion (`L: assert (c) begin
1617/// ... disable L; ... end`, a block named `L` holding only the assertion)
1618/// whose only `disable` targets `L` itself and which contains no other
1619/// suspend point. Disabling the label ends the assertion statement (LRM
1620/// 9.6.2: a labeled statement is a named scope), so the whole block is one
1621/// straight-line leaf for the CFG and codegen leaves it through an exit
1622/// block (`process_lower`'s `List | Block` arm).
1623pub fn self_disabling_assertion_block(stmt: &Statement<'_>) -> bool {
1624 if stmt.kind() != StatementKind::Block {
1625 return false;
1626 }
1627 let name = block_name(stmt);
1628 if name.is_empty() {
1629 return false;
1630 }
1631 let children = stmt.statements();
1632 let [only] = children.as_slice() else {
1633 return false;
1634 };
1635 if only.kind() != StatementKind::ImmediateAssertion {
1636 return false;
1637 }
1638 let mut disables = 0usize;
1639 let mut stack = vec![only.as_sem_node()];
1640 while let Some(n) = stack.pop() {
1641 if let Some(s) = n.as_statement()
1642 && s.kind() == StatementKind::Disable
1643 {
1644 match disable_label(&s) {
1645 Ok(l) if l == name => disables += 1,
1646 _ => return false,
1647 }
1648 }
1649 stack.extend(live_children(&n));
1650 }
1651 disables > 0
1652 && !contains_suspend_trigger_in(
1653 only.as_sem_node(),
1654 &mut std::collections::HashSet::new(),
1655 true,
1656 false,
1657 )
1658}
1659
1660/// The CFG of the plan that hosts the `expect` statement `stmt` for the
1661/// assertion engine (LRM 1800-2023 §16.17): `stmt` is an ordinary leaf of it,
1662/// not a wait -- the process that blocks on the `expect` is another plan, built
1663/// by [`build_cfg_clocked`].
1664pub fn build_cfg_hosted_assertion<'d>(
1665 stmt: &Statement<'d>,
1666 default_clocking: Option<Symbol<'d>>,
1667) -> Result<ProcessCfg<'d>, LoweringError> {
1668 let mut builder = CfgBuilder::new();
1669 builder.default_clocking = default_clocking;
1670 builder.expect_as_leaf = true;
1671 let entry = builder.alloc_block();
1672 let exit = builder.walk_stmt(*stmt, entry)?;
1673 builder.close(exit, Terminator::End { loop_back: None });
1674 Ok(ProcessCfg {
1675 blocks: builder.finish(),
1676 entry,
1677 body: *stmt,
1678 inlined_locals: Vec::new(),
1679 captures: Vec::new(),
1680 captured: Vec::new(),
1681 ref_bindings: Vec::new(),
1682 disable_names: Vec::new(),
1683 fork_branch_names: Vec::new(),
1684 disable_scopes: Vec::new(),
1685 disable_broadcast: Vec::new(),
1686 inherited: Vec::new(),
1687 capture_types: Vec::new(),
1688 recursion: Recursion::None,
1689 share_id: None,
1690 })
1691}
1692
1693/// Whether a call was written `super.m(...)` (LRM 8.15: not virtual).
1694pub type SuperDetector = fn(Expression<'_>) -> bool;
1695
1696thread_local! {
1697 /// The [`SuperDetector`]: recovering `super.` needs the design's syntax,
1698 /// which codegen holds (`sv_sim_codegen::classes::call_is_explicit_super`);
1699 /// the run installs it.
1700 static EXPLICIT_SUPER: std::cell::Cell<Option<SuperDetector>> =
1701 const { std::cell::Cell::new(None) };
1702}
1703
1704/// Installs (or, with `None`, removes) the `super.`-call detector
1705/// [`CfgBuilder::inline_task_call`] uses for virtual class tasks.
1706pub fn set_explicit_super_detector(f: Option<SuperDetector>) {
1707 EXPLICIT_SUPER.with(|c| c.set(f));
1708}
1709
1710fn is_explicit_super(call: Expression<'_>) -> bool {
1711 EXPLICIT_SUPER.with(|c| c.get().is_some_and(|f| f(call)))
1712}
1713
1714/// The design's class templates a dynamic type can be (every `new()`-reachable
1715/// class and its ancestors, with their dense template ids), for dispatching a
1716/// virtual class task that can suspend: such a task is inlined, so a call
1717/// through a base handle becomes a switch over the overrides the possible
1718/// dynamic classes resolve it to.
1719pub struct VirtualTasks<'d> {
1720 pub classes: Vec<(u32, sv_lang::Type<'d>)>,
1721 /// Per entry of `classes`, its base chain (root first, itself last) and
1722 /// the chain's symbol keys, filled on first use: `implementations` runs
1723 /// once per virtual method per fixed-point round, and rebuilding the
1724 /// chain and every ancestor's hierarchical path each time dominated the
1725 /// compile of a deep class library (`import uvm_pkg::*`).
1726 chains: std::cell::OnceCell<Vec<ClassChain<'d>>>,
1727 /// Per task, whether its body reaches itself ([`task_reaches`] with the
1728 /// task as target): decided once per task, not per call site, because
1729 /// each query walks every task reachable through virtual dispatch (a
1730 /// UVM testbench compiled for over an hour re-walking `uvm_pkg`).
1731 self_reach: std::cell::RefCell<std::collections::HashMap<crate::symid::SymbolId, bool>>,
1732}
1733
1734/// One class's base chain and the keys of its members ([`VirtualTasks`]).
1735struct ClassChain<'d> {
1736 chain: Vec<sv_lang::Type<'d>>,
1737 keys: std::collections::HashSet<crate::symid::SymbolId>,
1738}
1739
1740thread_local! {
1741 /// Every virtual method one of whose implementations (in the classes a
1742 /// dynamic type can be) can suspend ([`set_suspending_virtuals`]).
1743 static SUSPENDING_VIRTUALS: std::cell::RefCell<std::collections::HashSet<crate::symid::SymbolId>> =
1744 std::cell::RefCell::new(std::collections::HashSet::new());
1745}
1746
1747fn is_suspending_virtual(sub: &Symbol<'_>) -> bool {
1748 SUSPENDING_VIRTUALS.with(|s| s.borrow().contains(&sub.uid()))
1749}
1750
1751/// Records which virtual methods can suspend through an override (LRM 8.20:
1752/// a call runs the dynamic class's implementation, so it suspends when any
1753/// implementation can): such a call is inlined as a dispatch over the
1754/// overrides. To a fixed point, since an implementation may suspend only
1755/// through another virtual call. Clears the previous design's record.
1756pub fn set_suspending_virtuals(vt: &VirtualTasks<'_>) {
1757 SUSPENDING_VIRTUALS.with(|s| s.borrow_mut().clear());
1758 let mut methods: Vec<(crate::symid::SymbolId, Symbol<'_>)> = Vec::new();
1759 for (_, ty) in &vt.classes {
1760 for m in ty.as_symbol().members() {
1761 let m = if m.kind() == SymbolKind::MethodPrototype {
1762 match m.method_prototype_subroutine() {
1763 Some(real) => real,
1764 None => continue,
1765 }
1766 } else {
1767 m
1768 };
1769 if m.kind() == SymbolKind::Subroutine
1770 && (m.subroutine_this_var().is_some()
1771 || m.subroutine_flags().contains(sv_lang::MethodFlags::PURE))
1772 && (m.subroutine_flags().contains(sv_lang::MethodFlags::VIRTUAL)
1773 || m.subroutine_override().is_some())
1774 && !methods.iter().any(|(_, x)| *x == m)
1775 {
1776 methods.push((m.uid(), m));
1777 }
1778 }
1779 }
1780 loop {
1781 let mut changed = false;
1782 for (uid, m) in &methods {
1783 if SUSPENDING_VIRTUALS.with(|s| s.borrow().contains(uid)) {
1784 continue;
1785 }
1786 if vt
1787 .implementations(m)
1788 .iter()
1789 .any(|(_, imp)| subroutine_can_suspend(imp))
1790 {
1791 SUSPENDING_VIRTUALS.with(|s| s.borrow_mut().insert(uid.clone()));
1792 changed = true;
1793 }
1794 }
1795 if !changed {
1796 break;
1797 }
1798 }
1799}
1800
1801impl<'d> VirtualTasks<'d> {
1802 /// The possible dynamic classes (template id, class type).
1803 pub fn new(classes: Vec<(u32, sv_lang::Type<'d>)>) -> Self {
1804 VirtualTasks {
1805 classes,
1806 chains: std::cell::OnceCell::new(),
1807 self_reach: std::cell::RefCell::new(std::collections::HashMap::new()),
1808 }
1809 }
1810
1811 /// The implementations of `method` the dynamic classes derived from (or
1812 /// equal to) its declaring class run, each with the template ids that
1813 /// run it: per class, the most-derived declaration of the name with a
1814 /// body.
1815 pub fn implementations(&self, method: &Symbol<'d>) -> Vec<(Vec<u32>, Symbol<'d>)> {
1816 let Some(decl) = method.parent() else {
1817 return Vec::new();
1818 };
1819 let name = method.name();
1820 let decl_key = decl.uid();
1821 let chains = self.chains.get_or_init(|| {
1822 self.classes
1823 .iter()
1824 .map(|(_, ty)| {
1825 let mut chain = crate::netlist::base_chain(*ty);
1826 chain.push(*ty);
1827 let keys = chain.iter().map(|c| c.as_symbol().uid()).collect();
1828 ClassChain { chain, keys }
1829 })
1830 .collect()
1831 });
1832 let mut out: Vec<(Vec<u32>, Symbol<'d>)> = Vec::new();
1833 for ((id, _), cc) in self.classes.iter().zip(chains) {
1834 if !cc.keys.contains(&decl_key) {
1835 continue;
1836 }
1837 let found = cc.chain.iter().rev().find_map(|c| {
1838 let s = c.as_symbol().find(name)?;
1839 let s = if s.kind() == SymbolKind::MethodPrototype {
1840 s.method_prototype_subroutine()?
1841 } else {
1842 s
1843 };
1844 (s.kind() == SymbolKind::Subroutine
1845 && s.body().is_some()
1846 && !s.subroutine_flags().contains(sv_lang::MethodFlags::PURE))
1847 .then_some(s)
1848 });
1849 let Some(imp) = found else { continue };
1850 match out.iter_mut().find(|(_, s)| *s == imp) {
1851 Some((ids, _)) => ids.push(*id),
1852 None => out.push((vec![*id], imp)),
1853 }
1854 }
1855 out
1856 }
1857}
1858
1859/// [`build_cfg`] for one branch of the fork block named `fork_name`, if
1860/// named: a `disable` of that name inside the branch ends the fork.
1861#[allow(clippy::too_many_arguments)]
1862fn build_cfg_in<'d>(
1863 body: &Statement<'d>,
1864 loop_kind: ProcessLoopKind,
1865 fork_name: Option<String>,
1866 default_clocking: Option<Symbol<'d>>,
1867 peers: Option<&[PeerBlock]>,
1868 checker_clock: Option<InferredClockEvent<'d>>,
1869 inherited: Inherited<'d>,
1870 virtual_tasks: Option<std::rc::Rc<VirtualTasks<'d>>>,
1871) -> Result<ProcessCfg<'d>, LoweringError> {
1872 // LRM 9.6.2: the name(s) a `disable` ANYWHERE may single this CFG's own
1873 // process out by -- computed before `fork_name` moves into `builder`.
1874 let mut disable_names: Vec<String> = Vec::new();
1875 disable_names.extend(fork_name.clone());
1876 disable_names.extend(peer_block(body).map(|p| p.name));
1877
1878 let mut builder = CfgBuilder::new();
1879 builder.this_stack.extend(inherited.this_var);
1880 builder.ref_stack = inherited.ref_aliases;
1881 builder.global_clock = inherited.global_clock;
1882 builder.detached_trigger = inherited.detached_trigger;
1883 builder.forced_extern = inherited.forced_extern;
1884 builder.rec_outer = inherited.rec_outer;
1885 if let Some(memo) = inherited.fork_memo {
1886 builder.fork_memo = memo;
1887 }
1888 if let Some(rec) = inherited.rec {
1889 // The same capture temporaries the activation reserves.
1890 for _ in 0..rec.groups {
1891 for &(a, f) in &rec.proto {
1892 builder.set_capture_type(a, f);
1893 }
1894 }
1895 builder.rec = Some(rec);
1896 }
1897 builder.rec_up = inherited.rec_up;
1898 builder.virtual_tasks = virtual_tasks;
1899 builder.fork_name = fork_name;
1900 builder.fork_branch_names = inherited.known_fork_labels;
1901 // LRM 9.6.2: a branch also answers to `disable` of any block enclosing
1902 // its fork (it is an activity enabled within that block), so a block that
1903 // ends with detached branches still running ends them. Only names some
1904 // `disable` of the design mentions are tagged.
1905 builder.disable_names_extra(&inherited.ancestor_blocks, &mut disable_names);
1906 builder.body_node = Some(body.as_sem_node());
1907 builder.ancestor_blocks = inherited.ancestor_blocks;
1908 builder.default_clocking = default_clocking;
1909 builder.checker_clock = checker_clock;
1910 if let Some(peers) = peers {
1911 // A fork branch is not the process's own scope: only a whole process
1912 // has an outermost scope, assertion labels and peers.
1913 builder.peers = peers.to_vec();
1914 builder.assertion_labels = assertion_labels(body)
1915 .into_iter()
1916 .map(|(name, _)| name)
1917 .collect();
1918 builder.deferred_labels = deferred_assertion_labels(body)
1919 .into_iter()
1920 .map(|(name, _)| name)
1921 .collect();
1922 builder.immediate_labels = immediate_assertion_labels(body);
1923 builder.outermost = peer_block(body).map(|p| p.name);
1924 }
1925 let entry = builder.alloc_block();
1926 let exit = builder.walk_stmt(*body, entry)?;
1927 let loop_back = match loop_kind {
1928 ProcessLoopKind::OneShot => None,
1929 ProcessLoopKind::Looping => Some(entry),
1930 };
1931 builder.close(exit, Terminator::End { loop_back });
1932 let ref_bindings = builder.take_ref_bindings();
1933 let inlined_locals = std::mem::take(&mut builder.inlined_locals);
1934 let captures = std::mem::take(&mut builder.captures);
1935 let capture_types = std::mem::take(&mut builder.capture_types);
1936 let fork_branch_names = std::mem::take(&mut builder.fork_branch_names);
1937 let disable_scopes = builder.take_disable_scopes();
1938 let disable_broadcast = std::mem::take(&mut builder.disable_broadcast);
1939 Ok(ProcessCfg {
1940 blocks: builder.finish(),
1941 entry,
1942 body: *body,
1943 inlined_locals,
1944 captures,
1945 captured: Vec::new(),
1946 ref_bindings,
1947 disable_names,
1948 fork_branch_names,
1949 disable_scopes,
1950 disable_broadcast,
1951 inherited: Vec::new(),
1952 capture_types,
1953 recursion: Recursion::None,
1954 share_id: None,
1955 })
1956}
1957
1958/// The recursive task whose activation a builder is walking.
1959#[derive(Clone)]
1960struct RecursiveTask<'d> {
1961 task: crate::symid::SymbolId,
1962 /// The [`ProcessCfg::share_id`] of this activation's Root (unique per
1963 /// distinct [`RootKey`]); part of the key of every Root nested in it.
1964 share: u64,
1965 /// The first call's actuals and the formals they shape (the capture
1966 /// temporaries' types, [`CfgBuilder::set_capture_type`]), so a fork
1967 /// branch inside the activation can reserve the same temporaries.
1968 proto: Vec<(Expression<'d>, Symbol<'d>)>,
1969 /// The number of formals: capture temporaries `0..n` hold the incoming
1970 /// arguments, `n..2n` the arguments of a recursive call.
1971 n: u32,
1972 /// The number of temporary groups of `n`: 2, or 4 when a formal is an
1973 /// `output`/`inout` (`2n..3n` the activation's results, `3n..4n` those
1974 /// of a recursive call).
1975 groups: u32,
1976 /// Per formal: whether it is an `output`/`inout`.
1977 outs: Vec<bool>,
1978 /// Per formal: the static storage an aliased `ref` formal names.
1979 refs: Vec<Option<Symbol<'d>>>,
1980 /// A class method: capture temporary 0 (formal 0) is its `this`.
1981 has_this: bool,
1982 body: Statement<'d>,
1983}
1984
1985/// Everything a fork branch's CFG is a function of besides its statement:
1986/// the key of [`CfgBuilder::fork_memo`]. A branch is walked by a fresh
1987/// builder seeded only from [`Inherited`], so equal keys give equal CFGs.
1988#[derive(PartialEq, Eq, Hash)]
1989struct ForkBranchKey {
1990 /// Address and kind of the branch statement's AST node.
1991 stmt: (usize, u16),
1992 fork_name: Option<String>,
1993 default_clocking: Option<crate::symid::SymbolId>,
1994 this_var: Option<crate::symid::SymbolId>,
1995 ref_aliases: Vec<(crate::symid::SymbolId, crate::symid::SymbolId)>,
1996 rec_outer: Vec<crate::symid::SymbolId>,
1997 labels: Vec<String>,
1998 ancestors: Vec<String>,
1999}
2000
2001/// Everything a [`Recursion::Root`] activation's CFG is a function of: the
2002/// key of its entry in [`CfgBuilder::fork_memo`]. A Root is walked by a fresh
2003/// builder seeded from exactly these fields (the call site's actuals only
2004/// supply per-site capture bindings; the capture temporaries are typed by
2005/// the formals), so equal keys give equal CFGs. `chain` is the
2006/// [`RecursiveTask::share`] ids of the enclosing activations, which pins
2007/// each one's CFG (and so its entry block and frame layout, which an
2008/// [`Recursion::AncestorSpawn`] inside this Root copies from).
2009#[derive(PartialEq, Eq, Hash)]
2010struct RootKey {
2011 task: crate::symid::SymbolId,
2012 this_var: Option<crate::symid::SymbolId>,
2013 default_clocking: Option<crate::symid::SymbolId>,
2014 rec_outer: Vec<crate::symid::SymbolId>,
2015 chain: Vec<u64>,
2016 ref_aliases: Vec<(crate::symid::SymbolId, crate::symid::SymbolId)>,
2017 labels: Vec<String>,
2018 ancestors: Vec<String>,
2019}
2020
2021/// A [`CfgBuilder::fork_memo`] key: a fork branch or a recursive-task Root.
2022#[derive(PartialEq, Eq, Hash)]
2023enum MemoKey {
2024 Fork(ForkBranchKey),
2025 Root(RootKey),
2026}
2027
2028/// The fork branches and recursive-task Roots of one process already built,
2029/// by [`MemoKey`].
2030type ForkMemo<'d> =
2031 std::rc::Rc<std::cell::RefCell<std::collections::HashMap<MemoKey, ProcessCfg<'d>>>>;
2032
2033/// Source of [`ProcessCfg::share_id`]s (unique across the program).
2034static NEXT_SHARE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
2035
2036/// The AST node address of a statement handle (a handle is a copy of the
2037/// FFI node pointer; two handles to one node compare equal here).
2038fn stmt_addr(s: &Statement<'_>) -> usize {
2039 // SAFETY: `Statement` wraps one `repr(C)` `slang_ast` whose first field
2040 // is the node pointer; reading that word reads the pointer.
2041 unsafe { *std::ptr::from_ref(s).cast::<usize>() }
2042}
2043
2044/// What a fork branch inherits from the inlined task it is forked in: its
2045/// `this` and its aliased `ref` formals.
2046#[derive(Default)]
2047struct Inherited<'d> {
2048 this_var: Option<Symbol<'d>>,
2049 ref_aliases: Vec<(Symbol<'d>, Symbol<'d>)>,
2050 /// [`CfgBuilder::global_clock`].
2051 global_clock: Option<InferredClockEvent<'d>>,
2052 /// [`CfgBuilder::detached_trigger`].
2053 detached_trigger: bool,
2054 /// [`CfgBuilder::forced_extern`].
2055 forced_extern: Option<Symbol<'d>>,
2056 /// [`CfgBuilder::rec_outer`].
2057 rec_outer: Vec<crate::symid::SymbolId>,
2058 /// [`CfgBuilder::fork_memo`], shared by a whole process's builders.
2059 fork_memo: Option<ForkMemo<'d>>,
2060 /// [`CfgBuilder::rec`]: a fork branch inside a recursive activation can
2061 /// itself call the task (each such call spawns the activation again).
2062 rec: Option<RecursiveTask<'d>>,
2063 /// [`CfgBuilder::rec_up`].
2064 rec_up: Vec<RecursiveTask<'d>>,
2065 /// [`CfgBuilder::fork_branch_names`]'s own starting point: every label
2066 /// already known reachable from here (the parent's own accumulated list
2067 /// at the fork site, plus this fork's sibling branches) before this
2068 /// CFG's own walk adds any it discovers deeper in its own body.
2069 known_fork_labels: Vec<String>,
2070 /// [`CfgBuilder::ancestor_blocks`]' starting point.
2071 ancestor_blocks: Vec<String>,
2072}
2073
2074/// A VPI frame probe of an inlined blocking task (37.43): its activation
2075/// begins (after its arguments and locals are set up) or ends.
2076#[derive(Clone, Debug)]
2077pub enum FrameProbe<'d> {
2078 Enter {
2079 task: Symbol<'d>,
2080 /// Its automatic variables: formals, then locals.
2081 vars: Vec<Symbol<'d>>,
2082 /// The `ref` formals the call bound by copy-in/copy-out (an actual
2083 /// that is not static storage) with their actuals, in formal order
2084 /// (37.15 detail 3). They are ref objs of the frame, not variables
2085 /// (so absent from `vars`).
2086 bound: Vec<(Symbol<'d>, Expression<'d>)>,
2087 },
2088 Exit {
2089 task: Symbol<'d>,
2090 vars: Vec<Symbol<'d>>,
2091 },
2092}
2093
2094struct PartialBlock<'d> {
2095 bindings: Vec<ArgBinding<'d>>,
2096 this_var: Option<Symbol<'d>>,
2097 ref_aliases: Vec<(Symbol<'d>, Symbol<'d>)>,
2098 stmts: Vec<Statement<'d>>,
2099 probes: Vec<(usize, Statement<'d>)>,
2100 disable_probes: Vec<(usize, Statement<'d>)>,
2101 frame_probes: Vec<(usize, FrameProbe<'d>)>,
2102 block_events: Vec<(usize, Symbol<'d>, bool)>,
2103 stmt_scopes: Vec<(usize, Symbol<'d>)>,
2104 step_exprs: Vec<Expression<'d>>,
2105 repeat_init: Option<(RepeatCounterId, RepeatCount<'d>)>,
2106 repeat_decrement: Option<RepeatCounterId>,
2107 expect_resume: bool,
2108 terminator: Option<Terminator<'d>>,
2109}
2110
2111struct CfgBuilder<'d> {
2112 blocks: Vec<PartialBlock<'d>>,
2113 /// Task 1: the single clock signal+edge `##N` (`CycleDelay`) is lowered
2114 /// against, inferred from the first `posedge`/`negedge` `SuspendPoint::
2115 /// Edge` this walk encounters (in practice, the enclosing `always_ff`'s
2116 /// own edge). `None` until seen; becomes permanently ambiguous
2117 /// (`Some(None)`) the moment a *second*, differently-referenced edge
2118 /// signal (or the SAME signal with a different edge -- Task 6 extends
2119 /// the ambiguity check to cover this too, since `CycleDelay` now carries
2120 /// a real `edge` field it must be unambiguous about) is seen, so a later
2121 /// `##N` inside a multi-clock or unclocked-context process is a named
2122 /// `LoweringError`, never a silent guess (`walk_timed`'s `CycleDelay`
2123 /// arm). Task 6: also threaded through as real `SuspendPoint::
2124 /// CycleDelay{clock, edge, ..}` payload -- previously computed
2125 /// then discarded once the ambiguity check passed.
2126 inferred_clock: Option<Option<(Expression<'d>, EdgeKind)>>,
2127 /// R-run Task 8: the next fresh [`RepeatCounterId`] this builder will
2128 /// mint -- dense, allocation-order-numbered, mirrors [`Self::blocks`]'s
2129 /// own `len()`-as-next-id discipline for [`BlockId`].
2130 next_repeat_counter: u32,
2131 /// Formals/locals of every task inlined so far (`ProcessCfg::
2132 /// inlined_locals`).
2133 inlined_locals: Vec<Symbol<'d>>,
2134 /// Every inlined call's `ref` formal with the symbol its actual names
2135 /// (`None` for an actual that is not a plain name): resolved into
2136 /// `ProcessCfg::ref_bindings` once the whole process is walked.
2137 ref_candidates: Vec<(Symbol<'d>, Option<Symbol<'d>>)>,
2138 /// Set while walking a block whose automatic variables were just
2139 /// bound on entry (so the block itself is walked normally).
2140 entering_block: bool,
2141 /// The call being inlined, whose type shapes the `this` capture of a
2142 /// recursive method call with an implicit receiver.
2143 call_anchor: Option<Expression<'d>>,
2144 /// The capture temporary the next [`Self::inline_task_call`] parks the
2145 /// called function's result in ([`ArgBinding::CallResult`]).
2146 pending_result_temp: Option<(u32, Expression<'d>)>,
2147 /// Where a `return;` inside the innermost task being inlined goes.
2148 return_targets: Vec<BlockId>,
2149 /// The result variable of each inlined function (`None` for a task or void
2150 /// function), parallel to the inlined entries of [`Self::return_targets`]:
2151 /// `return expr;` assigns it (LRM 1800-2023 13.4.1).
2152 ret_var_stack: Vec<Option<Symbol<'d>>>,
2153 /// The `this` of each class-method task being inlined, innermost last
2154 /// ([`BasicBlock::this_var`]).
2155 this_stack: Vec<Symbol<'d>>,
2156 /// The `ref` formals of the tasks being inlined that alias their call's
2157 /// storage ([`BasicBlock::ref_aliases`]), innermost last.
2158 ref_stack: Vec<(Symbol<'d>, Symbol<'d>)>,
2159 /// The class templates a virtual suspending task can dispatch over.
2160 virtual_tasks: Option<std::rc::Rc<VirtualTasks<'d>>>,
2161 /// The type source of each capture temporary ([`ProcessCfg::capture_types`]).
2162 capture_types: Vec<Option<Symbol<'d>>>,
2163 /// Tasks currently being inlined (recursion guard).
2164 inline_stack: Vec<crate::symid::SymbolId>,
2165 /// While building a recursive task's activation
2166 /// ([`Self::inline_recursive_task`]): the task, whose calls are forks.
2167 rec: Option<RecursiveTask<'d>>,
2168 /// The recursive activations enclosing [`Self::rec`]'s (outermost first):
2169 /// a call of one of their tasks is a spawn of that activation
2170 /// ([`Recursion::AncestorSpawn`]).
2171 rec_up: Vec<RecursiveTask<'d>>,
2172 /// The recursive tasks whose activation encloses this builder's CFG (a
2173 /// fork branch inside one cannot start another activation of it).
2174 rec_outer: Vec<crate::symid::SymbolId>,
2175 /// The innermost enclosing loop's `continue` and `break` targets.
2176 loop_targets: Vec<(BlockId, BlockId)>,
2177 /// The sequential blocks being walked that declare automatic variables
2178 /// holding class or collection handles, innermost last (LRM 6.21: such a
2179 /// variable ends with its block, and a handle is released then).
2180 owned_scopes: Vec<Vec<Symbol<'d>>>,
2181 /// For each jump target (`break`/`continue`/`return`/`disable` exit), how
2182 /// many [`Self::owned_scopes`] were open outside it: a jump to it
2183 /// releases the scopes above that depth ([`Self::jump`]).
2184 target_depth: std::collections::HashMap<BlockId, usize>,
2185 /// The enclosing named blocks a `disable` may leave: `(name, exit)`,
2186 /// innermost last.
2187 /// `(label, exit, frames)`: `frames` is how many inlined tasks were
2188 /// active (`frame_stack`) when the block was entered.
2189 disable_targets: Vec<(String, BlockId, usize)>,
2190 /// The body this builder walks, for the lazy [`Self::body_forks`] scan.
2191 body_node: Option<SemNode<'d>>,
2192 /// The names of the fork blocks this builder walked (a subset of
2193 /// [`Self::fork_branch_names`]): a `fl.b.x` path through one is an
2194 /// instance-path-style label, not a branch name.
2195 fork_block_names: Vec<String>,
2196 /// Whether the body (or a task it calls) holds a fork block: decided on
2197 /// the first `disable` that jumps out of a named block, since only then
2198 /// can detached branches need ending with it (LRM 9.6.2).
2199 body_forks: Option<bool>,
2200 /// The covergroup-event blocks and inlined tasks being walked, innermost
2201 /// last, each with `disable_targets.len()` when it was entered: a
2202 /// `disable` that jumps out of them ends them there (LRM 9.6.2, 19.3), so
2203 /// [`Self::left_block_events`] signals their `end` at the jump.
2204 event_stack: Vec<(Symbol<'d>, usize)>,
2205 /// The tasks being inlined, innermost last, with their automatic
2206 /// variables (for the VPI frame probes a `disable` jumping out of them
2207 /// owes).
2208 frame_stack: Vec<(Symbol<'d>, Vec<Symbol<'d>>)>,
2209 /// The named blocks and inlined task activations being walked whose name
2210 /// some `disable` of the design targets, `(label, exit)`, innermost last:
2211 /// every block allocated while one is open is inside it
2212 /// ([`Self::block_scopes`], [`ProcessCfg::disable_scopes`]).
2213 /// `frames` is how many inlined tasks were active ([`Self::frame_stack`])
2214 /// outside the scope.
2215 scope_chain: Vec<(String, BlockId, usize)>,
2216 /// The names of the named sequential blocks being walked, outermost first
2217 /// (the path `disable p.b.x;` names, LRM 9.6.2).
2218 name_path: Vec<String>,
2219 /// [`Self::scope_chain`] as it was when each block was allocated.
2220 block_scopes: Vec<Vec<(String, BlockId, usize)>>,
2221 /// [`Self::frame_stack`] as it was when each block was allocated (empty
2222 /// outside every scope).
2223 block_frames: Vec<Vec<(Symbol<'d>, Vec<Symbol<'d>>)>>,
2224 /// [`ProcessCfg::disable_broadcast`].
2225 disable_broadcast: Vec<(String, bool)>,
2226 /// When this CFG is one branch of a named fork block: that name, which
2227 /// a `disable` inside it may target (`sv_rt_disable_fork_block`).
2228 fork_name: Option<String>,
2229 /// Every fork-branch [`ProcessCfg::disable_names`] a `disable <label>;`
2230 /// written anywhere in THIS CFG's own remaining walk may legitimately
2231 /// target (LRM 9.6.2), besides a same-process lexically enclosing block
2232 /// or this CFG's own `fork_name`: seeded from [`Inherited::
2233 /// known_fork_labels`] (what the parent already knew, plus this fork's
2234 /// siblings, when this CFG is itself a branch), then extended, going
2235 /// forward, by every later fork THIS walk itself builds branches for --
2236 /// never popped (unlike `disable_targets`): LRM 9.6.2 lets a branch's
2237 /// name be disabled for the rest of the process's life, not only while
2238 /// the `fork` statement that spawned it is still being walked.
2239 fork_branch_names: Vec<String>,
2240 /// The named blocks enclosing the fork statement(s) this CFG is a branch of,
2241 /// in the forking process(es) (LRM 9.6.2): a `disable` of one from inside
2242 /// the branch ends that block's whole activity, the forking process
2243 /// suspended at the join included, which resumes after the block.
2244 ancestor_blocks: Vec<String>,
2245 /// The name of the process's outermost scope (LRM 16.14.6.4), if it is a
2246 /// named block: a `disable` of it flushes the pending procedural assertion
2247 /// queue.
2248 outermost: Option<String>,
2249 /// The labels of the procedural concurrent assertions of this process: a
2250 /// `disable <label>;` clears the pending instances of that assertion
2251 /// (16.14.6.4).
2252 assertion_labels: Vec<String>,
2253 /// Labels of the process's deferred immediate assertions: `disable L;`
2254 /// cancels their pending reports (16.4.4), codegen lowers the kept statement.
2255 deferred_labels: Vec<String>,
2256 /// Labels of the process's plain immediate assertions: `disable L;` from
2257 /// outside the labeled statement is a no-op (it already finished).
2258 immediate_labels: Vec<String>,
2259 /// The outermost named blocks of the instance's other processes, which this
2260 /// process may `disable` (16.14.6.4, the `b3` example).
2261 peers: Vec<PeerBlock>,
2262 /// [`ProcessCfg::captures`].
2263 captures: Vec<Expression<'d>>,
2264 /// The instance's default clocking block, which `##N` counts.
2265 default_clocking: Option<Symbol<'d>>,
2266 /// While lowering the timing control of a clocking drive (`cb.q <= ##N v`):
2267 /// the drive target's own clocking block, whose event `##N` counts (LRM
2268 /// 1800-2023 14.16) instead of the default clocking's.
2269 drive_clocking: Option<Symbol<'d>>,
2270 /// The clock variable (`v.sb.c`) of that drive when it goes through a
2271 /// virtual interface member access, else `None` (see
2272 /// [`SuspendPoint::CycleDelay`]'s `via`).
2273 drive_via: Option<Expression<'d>>,
2274 /// Set by [`Self::walk_timed`] around its [`Self::suspend_point`] call: a
2275 /// run-time `##(expr)` count is then accepted (as a single `##1` wait the
2276 /// caller wraps in a counted loop) and its expression left in
2277 /// `dynamic_cycle_count`; every other caller still refuses it by name.
2278 allow_dynamic_cycle: bool,
2279 /// Set by [`Self::walk_timed`] around the [`Self::suspend_point`] of a
2280 /// mixed class-event / signal `@(a or b)` list: the class terms are then
2281 /// skipped (the leaf statement queues the process on them) and only the
2282 /// signal terms form the suspend point. Every other caller keeps the class
2283 /// term, so it cannot be dropped silently.
2284 skip_class_event_terms: bool,
2285 dynamic_cycle_count: Option<Expression<'d>>,
2286 /// What a checker procedure's `@clock` waits on when `clock` is the default
2287 /// `$inferred_clock` ([`InferredClockEvent`]).
2288 checker_clock: Option<InferredClockEvent<'d>>,
2289 /// What `@$global_clock` waits on: the event of the global clocking in
2290 /// effect for the process (14.14), when one is declared in its instance.
2291 global_clock: Option<InferredClockEvent<'d>>,
2292 /// Walking the body of the plan that hosts an `expect` statement for the
2293 /// assertion engine ([`build_cfg_hosted_assertion`]): the `expect` is then
2294 /// an ordinary leaf, not a blocking wait.
2295 expect_as_leaf: bool,
2296 /// This CFG is the detached process of a `->> @(ctl) e;` (LRM 1800-2023
2297 /// 15.5.2): its body statement is that trigger, which waits on `ctl` and
2298 /// then stages the nonblocking trigger, while the issuing process
2299 /// continues. Cleared once the body statement is walked.
2300 detached_trigger: bool,
2301 /// The fork branches this process has built so far ([`ForkBranchKey`]):
2302 /// a task inlined at many call sites repeats the same forks, and each
2303 /// distinct branch is built (and later lowered) once.
2304 fork_memo: ForkMemo<'d>,
2305 /// This CFG is one branch of a call to an `extern forkjoin` task (LRM
2306 /// 25.7.5): its body statement is that call, and it runs this one
2307 /// implementation. Cleared once the body statement is walked.
2308 forced_extern: Option<Symbol<'d>>,
2309}
2310
2311impl<'d> CfgBuilder<'d> {
2312 fn new() -> Self {
2313 Self {
2314 expect_as_leaf: false,
2315 detached_trigger: false,
2316 fork_memo: ForkMemo::default(),
2317 forced_extern: None,
2318 blocks: Vec::new(),
2319 inferred_clock: None,
2320 next_repeat_counter: 0,
2321 inlined_locals: Vec::new(),
2322 ref_candidates: Vec::new(),
2323 entering_block: false,
2324 return_targets: Vec::new(),
2325 ret_var_stack: Vec::new(),
2326 this_stack: Vec::new(),
2327 ref_stack: Vec::new(),
2328 virtual_tasks: None,
2329 inline_stack: Vec::new(),
2330 capture_types: Vec::new(),
2331 rec: None,
2332 rec_up: Vec::new(),
2333 rec_outer: Vec::new(),
2334 loop_targets: Vec::new(),
2335 owned_scopes: Vec::new(),
2336 target_depth: std::collections::HashMap::new(),
2337 disable_targets: Vec::new(),
2338 body_node: None,
2339 fork_block_names: Vec::new(),
2340 body_forks: None,
2341 event_stack: Vec::new(),
2342 call_anchor: None,
2343 pending_result_temp: None,
2344 frame_stack: Vec::new(),
2345 scope_chain: Vec::new(),
2346 name_path: Vec::new(),
2347 block_scopes: Vec::new(),
2348 block_frames: Vec::new(),
2349 disable_broadcast: Vec::new(),
2350 ancestor_blocks: Vec::new(),
2351 fork_name: None,
2352 fork_branch_names: Vec::new(),
2353 outermost: None,
2354 assertion_labels: Vec::new(),
2355 deferred_labels: Vec::new(),
2356 immediate_labels: Vec::new(),
2357 peers: Vec::new(),
2358 captures: Vec::new(),
2359 default_clocking: None,
2360 drive_clocking: None,
2361 drive_via: None,
2362 allow_dynamic_cycle: false,
2363 skip_class_event_terms: false,
2364 dynamic_cycle_count: None,
2365 checker_clock: None,
2366 global_clock: None,
2367 }
2368 }
2369
2370 fn alloc_block(&mut self) -> BlockId {
2371 let id = BlockId(self.blocks.len() as u32);
2372 let this_var = self.this_stack.last().copied();
2373 let ref_aliases = self.ref_stack.clone();
2374 self.block_scopes.push(self.scope_chain.clone());
2375 self.block_frames.push(if self.scope_chain.is_empty() {
2376 Vec::new()
2377 } else {
2378 self.frame_stack.clone()
2379 });
2380 self.blocks.push(PartialBlock {
2381 bindings: Vec::new(),
2382 this_var,
2383 ref_aliases,
2384 stmts: Vec::new(),
2385 probes: Vec::new(),
2386 disable_probes: Vec::new(),
2387 frame_probes: Vec::new(),
2388 block_events: Vec::new(),
2389 stmt_scopes: Vec::new(),
2390 step_exprs: Vec::new(),
2391 repeat_init: None,
2392 repeat_decrement: None,
2393 expect_resume: false,
2394 terminator: None,
2395 });
2396 id
2397 }
2398
2399 /// After a statement that called a subroutine, in a design where some
2400 /// function has `disable <block>;` (LRM 9.6.2, 35.9): leaves the enclosing
2401 /// named block the call disabled, if any ([`SwitchOn::FnDisable`]).
2402 /// Returns the block the statements go on in.
2403 fn fn_disable_check(&mut self, cur: BlockId) -> BlockId {
2404 let targets: Vec<(usize, String, BlockId, usize)> = self
2405 .disable_targets
2406 .iter()
2407 .enumerate()
2408 .rev()
2409 .filter(|(_, (n, _, _))| is_function_disable_label(n))
2410 .map(|(pos, (n, exit, frames))| (pos, n.clone(), *exit, *frames))
2411 .collect();
2412 if targets.is_empty() {
2413 return cur;
2414 }
2415 let next = self.alloc_block();
2416 let mut arms = Vec::new();
2417 let mut labels = Vec::new();
2418 for (pos, label, exit, frames) in targets {
2419 let arm = self.alloc_block();
2420 // The inlined tasks the jump leaves end here, innermost first.
2421 let left: Vec<_> = self.frame_stack[frames..].iter().rev().cloned().collect();
2422 for (task, vars) in left {
2423 self.frame_probe(arm, FrameProbe::Exit { task, vars });
2424 }
2425 self.left_block_events(arm, pos);
2426 self.jump(arm, exit);
2427 arms.push(arm);
2428 labels.push(label);
2429 }
2430 self.close(
2431 cur,
2432 Terminator::Switch {
2433 on: SwitchOn::FnDisable { labels },
2434 arms,
2435 default_blk: next,
2436 report: None,
2437 },
2438 );
2439 next
2440 }
2441
2442 fn push(&mut self, blk: BlockId, stmt: Statement<'d>) {
2443 let scope = self.frame_stack.last().map(|f| f.0);
2444 let b = &mut self.blocks[blk.0 as usize];
2445 if let Some(scope) = scope {
2446 b.stmt_scopes.push((b.stmts.len(), scope));
2447 }
2448 b.stmts.push(stmt);
2449 }
2450
2451 /// Records that `stmt` is about to execute at the current end of block
2452 /// `blk`'s statements ([`BasicBlock::probes`]).
2453 fn probe(&mut self, blk: BlockId, stmt: Statement<'d>) {
2454 let b = &mut self.blocks[blk.0 as usize];
2455 let at = b.stmts.len();
2456 b.probes.push((at, stmt));
2457 }
2458
2459 /// Records that the `disable` statement `stmt`, turned into a jump, has
2460 /// taken effect at the current end of block `blk`'s statements
2461 /// ([`BasicBlock::disable_probes`]).
2462 fn disable_probe(&mut self, blk: BlockId, stmt: Statement<'d>) {
2463 let b = &mut self.blocks[blk.0 as usize];
2464 let at = b.stmts.len();
2465 b.disable_probes.push((at, stmt));
2466 }
2467
2468 /// Appends to `out` the `blocks` some `disable` of the design names (a
2469 /// fork branch's extra [`ProcessCfg::disable_names`]).
2470 fn disable_names_extra(&self, blocks: &[String], out: &mut Vec<String>) {
2471 for b in blocks {
2472 if is_disabled_somewhere(b) && !out.contains(b) {
2473 out.push(b.clone());
2474 }
2475 }
2476 }
2477
2478 /// Whether the body being walked holds a fork block, in itself or in a
2479 /// task it calls (cached).
2480 fn forks_in_body(&mut self) -> bool {
2481 if let Some(v) = self.body_forks {
2482 return v;
2483 }
2484 let v = self.body_node.is_some_and(contains_fork_through_calls);
2485 self.body_forks = Some(v);
2486 v
2487 }
2488
2489 /// `disable a.b;` (LRM 9.6.2) whose segments are a contiguous chain of
2490 /// this process's lexically enclosing named blocks: a jump to the last
2491 /// one's exit, exactly like `disable b;`. Any other hierarchical target
2492 /// (an instance path, a block not enclosing the statement, or the
2493 /// outermost block / a task frame, whose lowering needs the plain label)
2494 /// stays loud.
2495 fn walk_hier_disable_of_enclosing(
2496 &mut self,
2497 stmt: Statement<'d>,
2498 cur: BlockId,
2499 path: &[String],
2500 ) -> Result<Option<BlockId>, LoweringError> {
2501 let loud = || {
2502 LoweringError::unsupported(format!(
2503 "disable of `{}` (a hierarchical name is supported only as a chain of the \
2504 statement's own lexically enclosing named blocks, not the process's \
2505 outermost block or a task frame; IEEE 1800-2023 9.6.2)",
2506 path.join(".")
2507 ))
2508 };
2509 let last = path.last().ok_or_else(loud)?;
2510 let Some(pos) = self.disable_targets.iter().rposition(|(n, _, _)| n == last) else {
2511 return Ok(None);
2512 };
2513 let chain_ok = pos + 1 >= path.len()
2514 && path
2515 .iter()
2516 .enumerate()
2517 .all(|(i, seg)| self.disable_targets[pos + 1 - path.len() + i].0 == *seg);
2518 if !chain_ok {
2519 return Ok(None);
2520 }
2521 let (_, exit, frames) = self.disable_targets[pos].clone();
2522 let outermost = pos == 0 && self.outermost.as_deref() == Some(last.as_str());
2523 if outermost || frames > 0 {
2524 return Err(loud());
2525 }
2526 self.disable_probe(cur, stmt);
2527 self.left_block_events(cur, pos);
2528 self.jump(cur, exit);
2529 Ok(Some(self.alloc_block()))
2530 }
2531
2532 /// The target of `disable a.b.c;` (LRM 9.6.2) when every segment names a
2533 /// block of ANOTHER process of this instance ([`PeerBlock`]): the last
2534 /// segment, handled like the plain `disable c;` of that peer. An instance
2535 /// path (or any segment that is not a peer block) stays loud, since the
2536 /// runtime resolves a label within the requester's own instance only.
2537 fn hier_peer_leaf(&self, path: &[String]) -> Result<String, LoweringError> {
2538 if path
2539 .iter()
2540 .all(|seg| self.peers.iter().any(|p| &p.name == seg))
2541 {
2542 return Ok(path[path.len() - 1].clone());
2543 }
2544 // `disable outer.inner;` / `disable top.outer.inner;` where `inner` is
2545 // a fork branch's own named block (a sibling or a later branch of this
2546 // process) or a block enclosing the fork, reached through blocks of
2547 // this process family (LRM 9.6.2): the same target as the plain
2548 // `disable inner;`. A leading segment that names none of them is the
2549 // instance's own name (only when a block segment follows it). Left as
2550 // the dotted text it would name a block of a child instance and match
2551 // nothing, a silent no-op.
2552 let known = |s: &String| {
2553 self.ancestor_blocks.contains(s)
2554 || self.fork_branch_names.contains(s)
2555 || self.disable_targets.iter().any(|(n, _, _)| n == s)
2556 || self.outermost.as_deref() == Some(s.as_str())
2557 };
2558 if let Some((leaf, mid)) = path.split_last()
2559 && known(leaf)
2560 {
2561 let skip = usize::from(mid.len() >= 2 && !known(&mid[0]));
2562 if mid[skip..].iter().all(known) {
2563 return Ok(leaf.clone());
2564 }
2565 }
2566 // `disable u1.blk;` (an instance path): the first segment names
2567 // neither a block of this or another process nor a task, so it is a
2568 // child instance. Kept as the dotted text; the runtime resolves it from
2569 // the requester's instance (the extra scope keys run.rs registers).
2570 let first = &path[0];
2571 if !self.peers.iter().any(|p| &p.name == first)
2572 && !self.disable_targets.iter().any(|(n, _, _)| n == first)
2573 && !is_task_name(first)
2574 && self.fork_name.as_deref() != Some(first.as_str())
2575 && !self
2576 .fork_branch_names
2577 .iter()
2578 .any(|n| n == first && !self.fork_block_names.contains(n))
2579 {
2580 return Ok(path.join("."));
2581 }
2582 // `disable p.b.x;` where `x` also names a block elsewhere: the dotted
2583 // path from another process's outermost block `p`, which the runtime
2584 // finds under the path-qualified scope label (`CfgBuilder::name_path`).
2585 if self.peers.iter().any(|p| &p.name == first) {
2586 return Ok(path.join("."));
2587 }
2588 Err(LoweringError::unsupported(format!(
2589 "disable of `{}` (a hierarchical name is supported only as a chain of the \
2590 statement's own lexically enclosing named blocks or of the named blocks of \
2591 another process of the same instance, not an instance path, the process's \
2592 outermost block or a task frame; IEEE 1800-2023 9.6.2)",
2593 path.join(".")
2594 )))
2595 }
2596
2597 /// Records the frame probe `probe` at the current end of block `blk`'s
2598 /// statements ([`BasicBlock::frame_probes`]).
2599 fn frame_probe(&mut self, blk: BlockId, probe: FrameProbe<'d>) {
2600 let b = &mut self.blocks[blk.0 as usize];
2601 let at = b.stmts.len();
2602 b.frame_probes.push((at, probe));
2603 }
2604
2605 /// Records the covergroup block event of `task` (LRM 19.3) at the current
2606 /// end of block `blk`'s statements ([`BasicBlock::block_events`]).
2607 fn block_event(&mut self, blk: BlockId, task: Symbol<'d>, is_end: bool) {
2608 let b = &mut self.blocks[blk.0 as usize];
2609 let at = b.stmts.len();
2610 b.block_events.push((at, task, is_end));
2611 }
2612
2613 /// A `disable` jumping to `disable_targets[pos]` ends every block and
2614 /// inlined task entered inside that target (LRM 9.6.2): records their
2615 /// `end` covergroup events (LRM 19.3) at the jump, innermost first. The
2616 /// target's own end event is the one at its exit block.
2617 fn left_block_events(&mut self, cur: BlockId, pos: usize) {
2618 let left: Vec<_> = self
2619 .event_stack
2620 .iter()
2621 .rev()
2622 .take_while(|(_, depth)| *depth > pos)
2623 .map(|(s, _)| *s)
2624 .collect();
2625 for s in left {
2626 self.block_event(cur, s, true);
2627 }
2628 }
2629
2630 fn push_step_expr(&mut self, blk: BlockId, expr: Expression<'d>) {
2631 self.blocks[blk.0 as usize].step_exprs.push(expr);
2632 }
2633
2634 /// R-run Task 8: mints a fresh, process-unique [`RepeatCounterId`] for a
2635 /// suspend-containing `repeat(N)` loop's own synthetic counter.
2636 fn alloc_repeat_counter(&mut self) -> RepeatCounterId {
2637 let id = RepeatCounterId(self.next_repeat_counter);
2638 self.next_repeat_counter += 1;
2639 id
2640 }
2641
2642 /// R-run Task 8: records `blk`'s own `repeat_init` -- see
2643 /// [`BasicBlock::repeat_init`]'s doc comment. `blk` must not already
2644 /// carry one (a builder-internal consistency check, like [`Self::close`]'s
2645 /// own "not already closed" invariant: every call site is
2646 /// `walk_repeat_loop`'s own `cur`, which allocates/owns exactly one
2647 /// `repeat_init` per call).
2648 fn set_repeat_init(&mut self, blk: BlockId, counter: RepeatCounterId, count: RepeatCount<'d>) {
2649 let slot = &mut self.blocks[blk.0 as usize].repeat_init;
2650 debug_assert!(
2651 slot.is_none(),
2652 "cfg builder tried to set block {blk:?}'s repeat_init twice"
2653 );
2654 *slot = Some((counter, count));
2655 }
2656
2657 /// R-run Task 8: records `blk`'s own `repeat_decrement` -- see
2658 /// [`BasicBlock::repeat_decrement`]'s doc comment.
2659 fn set_repeat_decrement(&mut self, blk: BlockId, counter: RepeatCounterId) {
2660 let slot = &mut self.blocks[blk.0 as usize].repeat_decrement;
2661 debug_assert!(
2662 slot.is_none(),
2663 "cfg builder tried to set block {blk:?}'s repeat_decrement twice"
2664 );
2665 *slot = Some(counter);
2666 }
2667
2668 /// Records `signal`/`edge` (from a just-built `SuspendPoint::Edge`) as
2669 /// the process's inferred single clock -- see `inferred_clock`'s own doc
2670 /// comment. Compares *by referenced symbol identity*, since two
2671 /// `Expression` handles for the same `clk` are not otherwise comparable;
2672 /// an edge with no resolvable symbol (e.g. a bit-select or expression,
2673 /// not a plain name) can never disambiguate a later `##N`, so it does
2674 /// not overwrite an already-known clock but also never establishes one
2675 /// on its own. Task 6: a second sighting of the SAME signal with a
2676 /// DIFFERENT edge also becomes ambiguous now (`CycleDelay` carries a
2677 /// real `edge` field it must be unambiguous about, unlike the
2678 /// pre-Task-6 bare cycle count).
2679 fn note_edge_signal(&mut self, signal: Expression<'d>, edge: EdgeKind) {
2680 let this_sym = signal.referenced_symbol().map(|s| s.uid());
2681 match &self.inferred_clock {
2682 None => {
2683 self.inferred_clock = Some(this_sym.is_some().then_some((signal, edge)));
2684 }
2685 Some(Some((existing, existing_edge))) => {
2686 let existing_sym = existing.referenced_symbol().map(|s| s.uid());
2687 if existing_sym.is_none() || this_sym != existing_sym || *existing_edge != edge {
2688 self.inferred_clock = Some(None); // now ambiguous
2689 }
2690 }
2691 Some(None) => {} // already ambiguous; stays ambiguous
2692 }
2693 }
2694
2695 /// Closes `blk` with `term`. `blk` must not already be closed -- every
2696 /// call site in this file only ever passes a `BlockId` it just allocated
2697 /// or one a nested `walk_stmt` call returned (which is itself always
2698 /// still-open by the same invariant), so this is a builder-internal
2699 /// consistency check, not a condition reachable from malformed SV source.
2700 fn close(&mut self, blk: BlockId, term: Terminator<'d>) {
2701 let slot = &mut self.blocks[blk.0 as usize].terminator;
2702 debug_assert!(
2703 slot.is_none(),
2704 "cfg builder tried to close block {blk:?} twice"
2705 );
2706 *slot = Some(term);
2707 }
2708
2709 /// Closes `cur` with a jump to `target`, releasing on the way the handles
2710 /// of the block-scoped automatic variables the jump leaves (a `break`,
2711 /// `continue`, `return` or `disable` out of a block, LRM 6.21).
2712 fn jump(&mut self, cur: BlockId, target: BlockId) {
2713 let open = self.owned_scopes.len();
2714 let depth = self.target_depth.get(&target).copied().unwrap_or(open);
2715 let vars: Vec<Symbol<'d>> = self.owned_scopes[depth.min(open)..]
2716 .iter()
2717 .rev()
2718 .flat_map(|s| s.iter().rev().copied())
2719 .collect();
2720 if vars.is_empty() {
2721 self.close(cur, Terminator::Fallthrough(target));
2722 return;
2723 }
2724 let leave = self.alloc_block();
2725 self.close(cur, Terminator::Fallthrough(leave));
2726 for var in vars {
2727 self.blocks[leave.0 as usize]
2728 .bindings
2729 .push(ArgBinding::Release { var });
2730 }
2731 self.close(leave, Terminator::Fallthrough(target));
2732 }
2733
2734 /// The static variable or net a `ref` actual names: itself, or what an
2735 /// enclosing inlined call's aliased `ref` formal it names stands for.
2736 fn ref_storage(&self, actual: Expression<'d>) -> Option<Symbol<'d>> {
2737 if !crate::symid::is_value_name(actual.kind()) {
2738 return None;
2739 }
2740 let sym = actual.referenced_symbol()?;
2741 if sym.kind() == SymbolKind::Net
2742 || (sym.kind() == SymbolKind::Variable
2743 && sym.variable_lifetime() == sv_lang::VariableLifetime::Static)
2744 {
2745 return Some(sym);
2746 }
2747 self.ref_stack
2748 .iter()
2749 .rev()
2750 .find(|(f, _)| f.uid() == sym.uid())
2751 .map(|&(_, a)| a)
2752 }
2753
2754 /// Resolves [`Self::ref_candidates`] into `ProcessCfg::ref_bindings`: a
2755 /// `ref` formal every call binds to one static variable or net (directly,
2756 /// or through an enclosing inlined task's `ref` formal that is itself
2757 /// bound) names that storage, so its copy-in/copy-out bindings and its
2758 /// frame local are dropped. A formal bound to different actuals, or to
2759 /// an automatic variable or a select, keeps the copy semantics.
2760 fn take_ref_bindings(&mut self) -> Vec<(Symbol<'d>, Symbol<'d>)> {
2761 use std::collections::HashMap;
2762 let storage = |s: &Symbol<'d>| {
2763 s.kind() == SymbolKind::Net
2764 || (s.kind() == SymbolKind::Variable
2765 && s.variable_lifetime() == sv_lang::VariableLifetime::Static)
2766 };
2767 // formal uid -> (formal, the one target symbol every call names), or
2768 // None once two calls disagree or one names no plain symbol.
2769 let mut targets: HashMap<crate::symid::SymbolId, (Symbol<'d>, Option<Symbol<'d>>)> =
2770 HashMap::new();
2771 let mut order = Vec::new();
2772 for (formal, named) in &self.ref_candidates {
2773 match targets.get_mut(&formal.uid()) {
2774 None => {
2775 order.push(formal.uid());
2776 targets.insert(formal.uid(), (*formal, *named));
2777 }
2778 Some((_, cur)) => {
2779 if cur.map(|c| c.uid()) != named.map(|n| n.uid()) {
2780 *cur = None;
2781 }
2782 }
2783 }
2784 }
2785 // Follow a chain through enclosing tasks' bound `ref` formals.
2786 let resolve = |start: Symbol<'d>| -> Option<Symbol<'d>> {
2787 let mut cur = start;
2788 for _ in 0..=targets.len() {
2789 if storage(&cur) {
2790 return Some(cur);
2791 }
2792 cur = targets.get(&cur.uid())?.1?;
2793 }
2794 None
2795 };
2796 let mut out = Vec::new();
2797 for uid in order {
2798 let (formal, named) = targets[&uid];
2799 if let Some(actual) = named.and_then(resolve) {
2800 out.push((formal, actual));
2801 }
2802 }
2803 let bound: std::collections::HashSet<crate::symid::SymbolId> =
2804 out.iter().map(|(f, _)| f.uid()).collect();
2805 for b in &mut self.blocks {
2806 b.bindings.retain(|x| match x {
2807 ArgBinding::In { formal, .. } | ArgBinding::Out { formal, .. } => {
2808 !bound.contains(&formal.uid())
2809 }
2810 _ => true,
2811 });
2812 }
2813 self.inlined_locals.retain(|l| !bound.contains(&l.uid()));
2814 out
2815 }
2816
2817 /// [`ProcessCfg::disable_scopes`] from the scopes each block was allocated in.
2818 ///
2819 /// A resume point inside inlined tasks the disabled scope does not contain
2820 /// goes through a block of its own that ends their VPI frames first,
2821 /// innermost first (37.43), like a `disable` jump inside the process does.
2822 fn take_disable_scopes(&mut self) -> Vec<(BlockId, String, BlockId)> {
2823 let scopes = std::mem::take(&mut self.block_scopes);
2824 let frames = std::mem::take(&mut self.block_frames);
2825 let mut out = Vec::new();
2826 for (i, (chain, active)) in scopes.into_iter().zip(frames).enumerate() {
2827 for (label, exit, outside) in chain {
2828 let target = if active.len() > outside {
2829 let leave = self.alloc_block();
2830 for (task, vars) in active[outside..].iter().rev().cloned() {
2831 self.frame_probe(leave, FrameProbe::Exit { task, vars });
2832 }
2833 self.close(leave, Terminator::Fallthrough(exit));
2834 leave
2835 } else {
2836 exit
2837 };
2838 out.push((BlockId(i as u32), label, target));
2839 }
2840 }
2841 out
2842 }
2843
2844 fn finish(self) -> Vec<BasicBlock<'d>> {
2845 self.blocks
2846 .into_iter()
2847 .map(|b| BasicBlock {
2848 bindings: b.bindings,
2849 this_var: b.this_var,
2850 ref_aliases: b.ref_aliases,
2851 stmts: b.stmts,
2852 probes: b.probes,
2853 disable_probes: b.disable_probes,
2854 frame_probes: b.frame_probes,
2855 block_events: b.block_events,
2856 stmt_scopes: b.stmt_scopes,
2857 step_exprs: b.step_exprs,
2858 repeat_init: b.repeat_init.clone(),
2859 repeat_decrement: b.repeat_decrement,
2860 expect_resume: b.expect_resume,
2861 terminator: b.terminator.expect(
2862 "internal invariant: build_cfg must close every block it allocates \
2863 before returning Ok",
2864 ),
2865 })
2866 .collect()
2867 }
2868
2869 /// PR-007's single recursive-descent walk. Returns the [`BlockId`] that
2870 /// is "current" for whatever statement follows `stmt` in its enclosing
2871 /// sequence. Every construct reachable in the A2 slice's restricted
2872 /// grammar falls through -- there is no `return`/`disable`/`break`/
2873 /// `continue` meaningfully reachable from a process body here (loops,
2874 /// functions and tasks are out of slice scope), so this never needs to
2875 /// signal "does not fall through".
2876 ///
2877 /// `sv_lang::kinds::StatementKind` is `#[non_exhaustive]` upstream (a
2878 /// `sv-lang-kinds` version bump can add a variant without breaking this
2879 /// crate's build), so a literal Rust `match` over it cannot itself be
2880 /// exhaustive-without-a-wildcard -- the compiler mandates a catch-all
2881 /// arm. Per this workspace's already-established pattern for these
2882 /// generated kind enums (`sv-sim-codegen::module`'s `TimingControlKind`/
2883 /// `StatementKind` handling, `determinism::kind_guard`'s doc comment),
2884 /// this is instead an `if`/`else-if` chain that names every one of
2885 /// today's 32 variants explicitly (`StatementKind::COUNT`, pinned by
2886 /// `determinism::kind_guard::statement_kind_count_matches_pinned_sv_lang_
2887 /// version`), falling through to a named [`LoweringError`] -- never a
2888 /// silent no-op -- only if a future version bump adds a 33rd variant
2889 /// this file has not been reviewed against (which that pinned-count test
2890 /// catches first).
2891 fn walk_stmt(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
2892 // A statically dead `if`/`case` arm slang could not bind demotes the
2893 // enclosing statements to `Invalid` wrappers; the real statement is
2894 // recoverable (see [`unwrap_invalid`]).
2895 let stmt = unwrap_invalid(stmt);
2896 let kind = stmt.kind();
2897
2898 if kind == StatementKind::Invalid {
2899 return Err(LoweringError::malformed(
2900 "Invalid statement node -- slang rejected this statement; an error inside a \
2901 statically dead `if`/`case` arm (e.g. a reversed range select `v[7:8]` of a \
2902 descending vector under a false parameter condition) is suppressed by slang \
2903 but still invalidates the whole enclosing procedure",
2904 ));
2905 }
2906 if kind == StatementKind::List {
2907 return self.walk_seq(live_statements(&stmt), cur);
2908 }
2909 // VPI `cbStmt` (38.36.1, Table 38-6): the statement is about to
2910 // execute. The detached process of `->> @(ctl) e;` runs the same
2911 // statement the issuer already probed. Loops are probed per iteration
2912 // by their own walkers (or by codegen when they stay a leaf).
2913 if is_entry_probe_kind(&stmt) && !self.detached_trigger {
2914 self.probe(cur, stmt);
2915 }
2916 if kind == StatementKind::Block {
2917 return self.walk_block(stmt, cur);
2918 }
2919 if kind == StatementKind::Conditional {
2920 return self.walk_conditional(stmt, cur);
2921 }
2922 if kind == StatementKind::Timed {
2923 return self.walk_timed(stmt, cur);
2924 }
2925 if kind == StatementKind::Wait {
2926 return self.walk_wait(stmt, cur);
2927 }
2928 if kind == StatementKind::WaitFork {
2929 // SC-125: a real suspend -- the scheduler resumes the process once
2930 // every child it forked has finished (`Suspend::WaitFork`).
2931 let resume = self.alloc_block();
2932 self.close(
2933 cur,
2934 Terminator::Suspend {
2935 point: SuspendPoint::WaitFork,
2936 resume_blk: resume,
2937 },
2938 );
2939 return Ok(resume);
2940 }
2941 if kind == StatementKind::ExpressionStatement && is_process_by_request_stmt(&stmt) {
2942 // Classes plan Task 13: `p.await();` / `p.suspend();`. The call
2943 // (an ordinary leaf statement) queues the request; the suspend
2944 // after it lets the scheduler apply it -- block in `await`,
2945 // park a self-suspend -- before the next statement runs.
2946 self.push(cur, stmt);
2947 let resume = self.alloc_block();
2948 self.close(
2949 cur,
2950 Terminator::Suspend {
2951 point: SuspendPoint::ByRequest,
2952 resume_blk: resume,
2953 },
2954 );
2955 // Classes plan Task 18: a mailbox `get`/`peek` delivers its message
2956 // into a variable, which a process that blocked cannot have done
2957 // when the call returned. The statement runs a second time after
2958 // the resume; the runtime tells the two phases apart (the first one
2959 // of a call that did not block leaves a marker, so the second is a
2960 // no-op: `sv_sim_sim::sched::mailbox`).
2961 if is_mailbox_two_phase_stmt(&stmt) {
2962 self.push(resume, stmt);
2963 }
2964 return Ok(resume);
2965 }
2966 if kind == StatementKind::WaitOrder {
2967 return self.walk_wait_order(stmt, cur);
2968 }
2969 if is_expect(&stmt) && !self.expect_as_leaf {
2970 return self.walk_expect(stmt, cur);
2971 }
2972 if kind == StatementKind::RandCase
2973 && (contains_suspend_trigger(stmt.as_sem_node())
2974 || escapes(
2975 stmt.as_sem_node(),
2976 !self.loop_targets.is_empty(),
2977 !self.return_targets.is_empty(),
2978 ))
2979 {
2980 return self.walk_randcase(stmt, cur);
2981 }
2982 if kind == StatementKind::EventTrigger {
2983 return self.walk_event_trigger(stmt, cur);
2984 }
2985 if kind == StatementKind::ExpressionStatement && async_pla_call(&stmt).is_some() {
2986 return self.walk_async_pla(stmt, cur);
2987 }
2988 if kind == StatementKind::DisableFork {
2989 // SC-125: never suspends -- an ordinary straight-line statement
2990 // that codegen lowers to a `sv_rt_disable_fork` call.
2991 self.push(cur, stmt);
2992 return Ok(cur);
2993 }
2994 if kind == StatementKind::Disable {
2995 // `disable blk;` inside `blk`: leave the block, like `break`
2996 // leaves a loop (LRM 1800-2023 §9.6.2). The target is read from
2997 // the statement's source text (`disable_label`).
2998 // `disable outer.inner;` (9.6.2 hierarchical block name) naming a
2999 // chain of this process's own lexically enclosing blocks is the
3000 // same jump as `disable inner;`.
3001 let mut label_override = None;
3002 if let Some(path) = disable_path(&stmt) {
3003 if let Some(next) = self.walk_hier_disable_of_enclosing(stmt, cur, &path)? {
3004 return Ok(next);
3005 }
3006 // A block nested in another process's scope, named through
3007 // that scope's blocks (`disable p.inner;`).
3008 label_override = Some(self.hier_peer_leaf(&path)?);
3009 }
3010 let label = match label_override {
3011 Some(l) => l,
3012 None => disable_label(&stmt)?,
3013 };
3014 // `disable f;` inside a branch of fork `f`: ends every branch
3015 // (codegen's runtime call), this one included.
3016 if !self.disable_targets.iter().any(|(n, _, _)| *n == label)
3017 && self.fork_name.as_deref() == Some(label.as_str())
3018 {
3019 self.push(cur, stmt);
3020 self.close(cur, Terminator::End { loop_back: None });
3021 return Ok(self.alloc_block());
3022 }
3023 if let Some(pos) = self
3024 .disable_targets
3025 .iter()
3026 .rposition(|(n, _, _)| *n == label)
3027 {
3028 let (_, exit, frames) = self.disable_targets[pos].clone();
3029 // 16.14.6.4: disabling the process's outermost scope also
3030 // flushes its pending procedural assertion queue; a nested
3031 // block does not. Codegen turns the kept statement into the
3032 // flush.
3033 let flush = pos == 0 && self.outermost.as_deref() == Some(label.as_str());
3034 // A task activation, or a block inside one, can be live in
3035 // other processes too (LRM 9.6.2: every activation ends): the
3036 // kept statement also reaches them at run time.
3037 let broadcast = frames > 0;
3038 // LRM 9.6.2: leaving the block ends the activities enabled
3039 // within it, the fork branches still running among them
3040 // (tagged with the block's name, `disable_names`).
3041 let ends_branches =
3042 !broadcast && self.forks_in_body() && is_disabled_somewhere(&label);
3043 if flush || broadcast || ends_branches {
3044 self.push(cur, stmt);
3045 }
3046 if broadcast || ends_branches {
3047 self.disable_broadcast.push((label.clone(), false));
3048 }
3049 self.disable_probe(cur, stmt);
3050 // The inlined tasks the jump leaves end here, innermost first.
3051 let left: Vec<_> = self.frame_stack[frames..].iter().rev().cloned().collect();
3052 for (task, vars) in left {
3053 self.frame_probe(cur, FrameProbe::Exit { task, vars });
3054 }
3055 self.left_block_events(cur, pos);
3056 self.jump(cur, exit);
3057 return Ok(self.alloc_block());
3058 }
3059 // `disable a1;` of a procedural concurrent assertion's label
3060 // (16.14.6.4) clears that assertion's pending instances and goes
3061 // on: the statement is kept for codegen.
3062 if self.assertion_labels.contains(&label) {
3063 self.push(cur, stmt);
3064 return Ok(cur);
3065 }
3066 // `disable a1;` of a deferred immediate assertion's label (16.4.4):
3067 // cancels its pending reports and goes on; kept for codegen.
3068 if self.deferred_labels.contains(&label) {
3069 self.push(cur, stmt);
3070 return Ok(cur);
3071 }
3072 // `disable L;` of a plain immediate assertion's label from outside
3073 // its statement: the statement has finished, nothing to end.
3074 if self.immediate_labels.contains(&label) {
3075 return Ok(cur);
3076 }
3077 // `disable b2;` of another process's outermost block (16.14.6.4,
3078 // the `b3` example): the other process cannot be inside a block
3079 // without a suspend point, so the disable's whole effect is that
3080 // process's queue flush.
3081 if let Some(can_suspend) = self
3082 .peers
3083 .iter()
3084 .find(|p| p.name == label)
3085 .map(|p| p.can_suspend)
3086 {
3087 self.push(cur, stmt);
3088 // A block with a suspend point may have its process inside it:
3089 // the disable then resumes that process after the block
3090 // (`sv_rt_disable_named`, `ProcessCfg::disable_scopes`).
3091 if can_suspend {
3092 self.disable_broadcast.push((label, true));
3093 }
3094 return Ok(cur);
3095 }
3096 // `disable u1.blk;` of a block of a child instance: every process
3097 // of that instance suspended inside the block resumes after it.
3098 if label.contains('.') {
3099 self.push(cur, stmt);
3100 self.disable_broadcast.push((label, true));
3101 return Ok(cur);
3102 }
3103 // `disable t;` of a task of the design that this process is not in:
3104 // it ends every activation of `t` in the other processes (LRM
3105 // 9.6.2) and the process goes on.
3106 if is_task_name(&label) {
3107 self.push(cur, stmt);
3108 self.disable_broadcast.push((label, true));
3109 return Ok(cur);
3110 }
3111 // LRM 9.6.2: `label` is a fork branch's own named outermost
3112 // block reachable from here -- the fork's parent (continuing
3113 // past it), a sibling branch, or a descendant of an ancestor
3114 // fork -- but not through a same-process lexically enclosing
3115 // block or this CFG's own `fork_name` (both already checked
3116 // above). Kept as an ordinary statement: codegen (`sv-sim-
3117 // codegen::process_lower`) tells this case apart from the ones
3118 // above by `ProcessLoweringPlan::fork_branch_names` and lowers
3119 // it to a runtime scan/kill of every live, so-tagged process
3120 // reachable from the caller's own fork family
3121 // (`sv_sim_sim::sched::SchedulerOps::disable_named`) -- never a
3122 // silent no-op when nothing is currently live (the same
3123 // "already finished, nothing to do" convention `disable fork;`
3124 // itself uses, LRM 9.6.3).
3125 if self.fork_branch_names.iter().any(|n| n == &label) {
3126 self.push(cur, stmt);
3127 return Ok(cur);
3128 }
3129 // LRM 9.6.2: `label` is a named block of the forking process
3130 // that encloses the fork this branch belongs to. The disable
3131 // ends all activity in the block: the siblings, this branch, and
3132 // the parent's wait at the join, which resumes after the block
3133 // (`sv_rt_disable_named`, the parent's `disable_scopes`). This
3134 // branch ends here, like `disable <fork name>;` above.
3135 if self.ancestor_blocks.iter().any(|n| n == &label) {
3136 self.push(cur, stmt);
3137 self.disable_broadcast.push((label, true));
3138 self.close(cur, Terminator::End { loop_back: None });
3139 return Ok(self.alloc_block());
3140 }
3141 return Err(LoweringError::unsupported(format!(
3142 "disable {label}; where `{label}` is not a named block or task enclosing it \
3143 in the same process, a procedural assertion label of it, a uniquely named \
3144 sequential block of another process or a task of the design (not a block \
3145 inside a fork branch of another process, nor a name shared by two blocks), \
3146 or a fork branch's own name reachable from here"
3147 )));
3148 }
3149 if kind == StatementKind::ForLoop {
3150 return self.walk_for_loop(stmt, cur);
3151 }
3152 if kind == StatementKind::WhileLoop {
3153 return self.walk_while_loop(stmt, cur);
3154 }
3155 if kind == StatementKind::DoWhileLoop {
3156 return self.walk_do_while_loop(stmt, cur);
3157 }
3158 if kind == StatementKind::ForeverLoop {
3159 return self.walk_forever_loop(stmt, cur);
3160 }
3161 if kind == StatementKind::ForeachLoop {
3162 return self.walk_foreach_loop(stmt, cur);
3163 }
3164 // R-run Task 8 (PR-045/PR-006): `repeat(N) body;` gets a real CFG
3165 // `Loop` shape ONLY when `body` contains a suspend point somewhere
3166 // in its own subtree -- a suspend inside a `repeat` needs its own
3167 // iteration counter to survive the resulting suspend/resume call
3168 // boundary (see `RepeatCounterId`'s own doc comment), which only a
3169 // real CFG split can provide. The suspend-free case stays exactly
3170 // the pre-existing opaque leaf `is_ordinary_statement_kind` already
3171 // routes to `stmt_shared::lower_repeat_loop` (R-run Task 4) --
3172 // falling through to that check below, unchanged, whenever this
3173 // condition is false, deliberately preserving
3174 // `repeat_loop_control_flow_shape_matches_between_function_and_
3175 // process_paths`'s own "a suspend-free repeat loop stays an
3176 // ordinary, un-decomposed leaf statement" invariant
3177 // (`shared_stmt_lowering.rs`).
3178 if kind == StatementKind::RepeatLoop
3179 && (contains_suspend_trigger(stmt.as_sem_node())
3180 || escapes(stmt.as_sem_node(), false, !self.return_targets.is_empty()))
3181 {
3182 return self.walk_repeat_loop(stmt, cur);
3183 }
3184
3185 // R-run Task 1: `$finish`/`$stop`/`$fatal` are a segment-closing
3186 // point, same category as a suspend -- LRM 1800-2023 §20.2/§20.10:
3187 // once one of these executes, none of the calling process's own
3188 // textually-following statements may ever run (not just the next
3189 // one in a flat statement list -- also not one nested arbitrarily
3190 // deep inside an `if`/`case`/loop branch this "ordinary straight-
3191 // line" arm below would otherwise happily keep pushing into the
3192 // SAME still-open block). This is checked before the generic
3193 // `is_ordinary_statement_kind` dispatch below (an `ExpressionStatement`
3194 // wrapping one of these calls is itself an ordinary-kind statement,
3195 // so without this earlier check it would fall straight into that
3196 // arm and keep accumulating every statement that follows it into
3197 // `cur`).
3198 //
3199 // Reuses the existing `Terminator::End { loop_back: None }` shape
3200 // (PR-069's "no new enum catch-all" extension policy) rather than a
3201 // new `Terminator::Halt`/`Terminator::Finish` variant: this closes
3202 // `cur` as a real process-kernel exit (`SegTerminator::End{loop_back:
3203 // None}` -> the compiled kernel returns `SEG_FINISHED` for this
3204 // call, per `lower_terminator`'s existing 1:1 mapping), unconditionally
3205 // `loop_back: None` even inside an `always`/`always_ff` process
3206 // (`ProcessLoopKind::Looping` would otherwise close the process's
3207 // FINAL block with `Some(entry)`) -- once `$finish`/`$stop`/`$fatal`
3208 // runs, the calling process itself must never be re-entered again,
3209 // regardless of what kind of process it is; the *global* simulation
3210 // shutdown that a bare `$finish`/`$stop` also requests is a
3211 // scheduler-level concern (`SchedulerOps::request_finish`,
3212 // `sv_rt_finish`'s own doc comment) this file has no visibility
3213 // into and does not need to model.
3214 //
3215 // Any statement textually following the call (whether later in this
3216 // same flat list, as `walk_seq`'s next loop iteration, or -- via
3217 // this function's own caller chain, e.g. `walk_conditional` closing
3218 // `then_exit`/`else_exit` with `Terminator::Fallthrough(join)` --
3219 // structurally nested deeper) is walked into a FRESH block instead
3220 // of `cur`: `cur` is already closed, so it can never be reached
3221 // again, and neither can anything later routed into the fresh
3222 // block, since nothing in this builder ever branches to a block
3223 // this function itself did not just hand back as "the current
3224 // exit" to some earlier, still-open predecessor -- there is none
3225 // here. The fresh block still gets a real terminator from whatever
3226 // caller eventually closes it (a plain, harmless `Fallthrough`/
3227 // `End` on dead code), satisfying `finish()`'s "every block must be
3228 // closed" invariant without special-casing unreachable blocks.
3229 // A call to a task whose body can suspend is inlined here, so its
3230 // timing controls become real suspend points of this process.
3231 if kind == StatementKind::ExpressionStatement
3232 && let Some(e) = stmt.expr()
3233 && crate::netlist::itf::is_vif_extern_call(&e)
3234 {
3235 return Err(LoweringError::unsupported(
3236 crate::netlist::itf::vif_extern_message(&e),
3237 ));
3238 }
3239 if kind == StatementKind::ExpressionStatement
3240 && let Some(e) = stmt.expr()
3241 && let Some(targets) = crate::netlist::itf::call_extern_targets(&e)
3242 && let Some(next) = self.walk_extern_call(stmt, e, &targets, cur)?
3243 {
3244 return Ok(next);
3245 }
3246 if kind == StatementKind::ExpressionStatement
3247 && let Some(e) = stmt.expr()
3248 && let Some(callee) = suspending_task_callee(&e)
3249 {
3250 return self.inline_task_call(e, callee, cur);
3251 }
3252 if matches!(kind, StatementKind::Break | StatementKind::Continue) {
3253 let &(cont, brk) = self
3254 .loop_targets
3255 .last()
3256 .ok_or_else(|| LoweringError::malformed(format!("{kind:?} outside any loop")))?;
3257 let target = if kind == StatementKind::Break {
3258 brk
3259 } else {
3260 cont
3261 };
3262 self.jump(cur, target);
3263 return Ok(self.alloc_block());
3264 }
3265 if (kind == StatementKind::Case || kind == StatementKind::PatternCase)
3266 && (contains_suspend_trigger(stmt.as_sem_node())
3267 || constant_case_live_arm(&stmt).is_some()
3268 || escapes(
3269 stmt.as_sem_node(),
3270 !self.loop_targets.is_empty(),
3271 !self.return_targets.is_empty(),
3272 ))
3273 {
3274 return self.walk_case(stmt, cur);
3275 }
3276 if kind == StatementKind::ImmediateAssertion
3277 && let Some(actions) = suspending_assertion_actions(&stmt)
3278 {
3279 return self.walk_immediate_assertion(stmt, actions, cur);
3280 }
3281 if kind == StatementKind::ExpressionStatement
3282 && let Some(e) = stmt.expr()
3283 && e.kind() == ExpressionKind::Assignment
3284 && e.is_nonblocking() != Some(true)
3285 && e.assignment_timing().is_some()
3286 {
3287 return self.walk_intra_assignment_timing(e, cur);
3288 }
3289 // `q <= @(e) v;` / `q <= repeat (n) @(e) v;` (LRM 10.4.2). A plain
3290 // delay stays a statement: the staged write carries it.
3291 if kind == StatementKind::ExpressionStatement
3292 && let Some(e) = stmt.expr()
3293 && e.kind() == ExpressionKind::Assignment
3294 && e.is_nonblocking() == Some(true)
3295 && e.assignment_timing()
3296 .and_then(|n| n.as_timing_control())
3297 .is_some_and(|tc| tc.kind() != TimingControlKind::Delay)
3298 {
3299 return self.walk_nba_event(stmt, e, cur);
3300 }
3301 if kind == StatementKind::Return
3302 && let Some(&target) = self.return_targets.last()
3303 {
3304 // `return;` inside an inlined task: jump past the task body.
3305 // `return expr;` of an inlined function first assigns its result
3306 // variable (in a block of its own: bindings run on block entry).
3307 let mut cur = cur;
3308 if self.ret_var_stack.len() == self.return_targets.len()
3309 && let Some(Some(rv)) = self.ret_var_stack.last().copied()
3310 && let Some(value) = stmt.expr()
3311 {
3312 if let Some(next) = self.hoist_suspending_calls_in(value.as_sem_node(), cur)? {
3313 cur = next;
3314 }
3315 let set = self.alloc_block();
3316 self.close(cur, Terminator::Fallthrough(set));
3317 self.blocks[set.0 as usize].bindings.push(ArgBinding::In {
3318 formal: rv,
3319 actual: value,
3320 });
3321 cur = set;
3322 }
3323 self.jump(cur, target);
3324 return Ok(self.alloc_block());
3325 }
3326 if kind == StatementKind::ExpressionStatement && is_finish_or_stop_call(&stmt) {
3327 self.push(cur, stmt);
3328 self.close(cur, Terminator::End { loop_back: None });
3329 return Ok(self.alloc_block());
3330 }
3331
3332 // PR-006's own acceptance text: "any other kind is ordinary
3333 // straight-line code". Kept as one opaque `Statement` leaf unless it
3334 // secretly contains a suspend-triggering construct somewhere in its
3335 // subtree, in which case there is no `Terminator::Loop`/`Case` to
3336 // represent the needed split (see this file's module doc comment's
3337 // extension policy) -- a named `LoweringError`, never a
3338 // silently-wrong CFG.
3339 if is_ordinary_statement_kind(kind) {
3340 if kind == StatementKind::ExpressionStatement
3341 && contains_suspend_trigger(stmt.as_sem_node())
3342 && let Some(next) = self.hoist_suspending_function_calls(stmt, cur)?
3343 {
3344 return Ok(next);
3345 }
3346 return if contains_suspend_trigger(stmt.as_sem_node())
3347 && !(is_expect(&stmt) && self.expect_as_leaf)
3348 {
3349 Err(LoweringError::unsupported(format!(
3350 "suspend point nested inside a {kind:?} statement (no CFG terminator \
3351 for this construct is implemented in this task, PR-069)"
3352 )))
3353 } else {
3354 self.push(cur, stmt);
3355 if kind == StatementKind::ExpressionStatement
3356 && has_function_disables()
3357 && contains_subroutine_call(stmt.as_sem_node())
3358 {
3359 return Ok(self.fn_disable_check(cur));
3360 }
3361 Ok(cur)
3362 };
3363 }
3364
3365 // Unreachable as long as `determinism::kind_guard`'s pinned
3366 // `StatementKind::COUNT == 32` still holds -- every one of today's 32
3367 // variants is named above or in `is_ordinary_statement_kind`. A
3368 // `sv-lang-kinds` bump that adds a 33rd fails that pinned-count test
3369 // loudly; until this file is reviewed and updated, reaching here is a
3370 // named error, never a silent fallthrough.
3371 Err(LoweringError::malformed(format!(
3372 "unrecognized StatementKind {kind:?} (sv-lang-kinds version drift -- see \
3373 determinism::kind_guard's StatementKind pin)"
3374 )))
3375 }
3376
3377 /// `expect (property) pass else fail;` (LRM 1800-2023 §16.17): blocks the
3378 /// process until the assertion engine has decided the property.
3379 ///
3380 /// The statement appears twice, as the assertion's two halves. In the block
3381 /// it is reached in, it queues the evaluation (codegen's
3382 /// `sv_rt_proc_assert`); the process then parks with no sensitivity at all
3383 /// (an empty [`SuspendPoint::EdgeList`]) until the engine, in the Observed
3384 /// region of the step it decides in, wakes it. In the block it resumes into
3385 /// ([`BasicBlock::expect_resume`]) it runs the pass or `else` statement, and
3386 /// whatever follows the `expect` goes on from there.
3387 fn walk_expect(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
3388 let blocking = [stmt.assertion_if_true(), stmt.assertion_if_false()]
3389 .into_iter()
3390 .flatten()
3391 .any(|a| contains_suspend_trigger(a.as_sem_node()));
3392 if blocking {
3393 return self.walk_expect_blocking(stmt, cur);
3394 }
3395 self.push(cur, stmt);
3396 let resume = self.alloc_block();
3397 self.blocks[resume.0 as usize].expect_resume = true;
3398 self.close(
3399 cur,
3400 Terminator::Suspend {
3401 point: SuspendPoint::EdgeList(Vec::new(), Vec::new()),
3402 resume_blk: resume,
3403 },
3404 );
3405 self.push(resume, stmt);
3406 Ok(resume)
3407 }
3408
3409 /// An `expect` whose pass or `else` action blocks (a delay, an event
3410 /// control, a wait, another `expect`; LRM 1800-2023 16.17 allows any
3411 /// statement there). The action cannot run inline in the resume
3412 /// segment, so the resume block switches on the outcome the engine woke
3413 /// the process with (`sv_rt_fired_term`: 0 pass, 1 fail, 2 -- actions
3414 /// switched off by `$assertcontrol`, 20.11 -- neither) and walks each
3415 /// action as ordinary blocks. A fail with no `else` reports the `$error`
3416 /// through a zero-arm switch, as `wait_order` does.
3417 fn walk_expect_blocking(
3418 &mut self,
3419 stmt: Statement<'d>,
3420 cur: BlockId,
3421 ) -> Result<BlockId, LoweringError> {
3422 self.push(cur, stmt);
3423 let resume = self.alloc_block();
3424 self.close(
3425 cur,
3426 Terminator::Suspend {
3427 point: SuspendPoint::EdgeList(Vec::new(), Vec::new()),
3428 resume_blk: resume,
3429 },
3430 );
3431 let join = self.alloc_block();
3432 let pass = self.alloc_block();
3433 let fail = self.alloc_block();
3434 let pass_exit = match stmt.assertion_if_true() {
3435 Some(s) => self.walk_stmt(s, pass)?,
3436 None => pass,
3437 };
3438 self.close(pass_exit, Terminator::Fallthrough(join));
3439 let fail_exit = match stmt.assertion_if_false() {
3440 Some(s) => self.walk_stmt(s, fail)?,
3441 None => {
3442 self.close(
3443 fail,
3444 Terminator::Switch {
3445 on: SwitchOn::FiredTerm,
3446 arms: Vec::new(),
3447 default_blk: join,
3448 report: Some(stmt.source_range().start),
3449 },
3450 );
3451 self.alloc_block()
3452 }
3453 };
3454 self.close(fail_exit, Terminator::Fallthrough(join));
3455 self.close(
3456 resume,
3457 Terminator::Switch {
3458 on: SwitchOn::FiredTerm,
3459 arms: vec![pass, fail, join],
3460 default_blk: join,
3461 report: None,
3462 },
3463 );
3464 Ok(join)
3465 }
3466
3467 /// A `randcase` whose items need their own blocks (one suspends, or
3468 /// escapes an enclosing loop or task): [`SwitchOn::RandCase`] picks one.
3469 fn walk_randcase(
3470 &mut self,
3471 stmt: Statement<'d>,
3472 cur: BlockId,
3473 ) -> Result<BlockId, LoweringError> {
3474 let join = self.alloc_block();
3475 let mut arms = Vec::new();
3476 for item in stmt.randcase_items() {
3477 let arm = self.alloc_block();
3478 arms.push(arm);
3479 let exit = self.walk_stmt(item.stmt(), arm)?;
3480 self.close(exit, Terminator::Fallthrough(join));
3481 }
3482 self.close(
3483 cur,
3484 Terminator::Switch {
3485 on: SwitchOn::RandCase(stmt),
3486 arms,
3487 default_blk: join,
3488 report: None,
3489 },
3490 );
3491 Ok(join)
3492 }
3493
3494 /// `wait_order(e1, ..., en) pass else fail;` (LRM 1800-2023 §15.5.4):
3495 /// step `i` waits on `e_i` .. `e_n`, and goes on only if `e_i` is the one
3496 /// that fired (the wait's term 0, [`SwitchOn::FiredTerm`]) -- any other
3497 /// fires out of order and fails. After `e_n`, the pass action; a
3498 /// failure without an `else` reports `$error`.
3499 fn walk_wait_order(
3500 &mut self,
3501 stmt: Statement<'d>,
3502 cur: BlockId,
3503 ) -> Result<BlockId, LoweringError> {
3504 let events = stmt.wait_order_events();
3505 if events.is_empty() {
3506 return Err(LoweringError::malformed("wait_order with no events"));
3507 }
3508 let pass = self.alloc_block();
3509 let fail = self.alloc_block();
3510 let join = self.alloc_block();
3511 let if_false = stmt.wait_order_if_false();
3512 let report = if_false.is_none().then(|| stmt.source_range().start);
3513 let mut at = cur;
3514 for i in 0..events.len() {
3515 let resume = self.alloc_block();
3516 self.close(
3517 at,
3518 Terminator::Suspend {
3519 point: SuspendPoint::EdgeList(
3520 events[i..]
3521 .iter()
3522 .map(|e| (*e, sv_lang::EdgeKind::None))
3523 .collect(),
3524 vec![Vec::new(); events.len() - i],
3525 ),
3526 resume_blk: resume,
3527 },
3528 );
3529 let next = if i + 1 == events.len() {
3530 pass
3531 } else {
3532 self.alloc_block()
3533 };
3534 self.close(
3535 resume,
3536 Terminator::Switch {
3537 on: SwitchOn::FiredTerm,
3538 arms: vec![next],
3539 default_blk: fail,
3540 report,
3541 },
3542 );
3543 at = next;
3544 }
3545 let pass_exit = match stmt.wait_order_if_true() {
3546 Some(s) => self.walk_stmt(s, pass)?,
3547 None => pass,
3548 };
3549 self.close(pass_exit, Terminator::Fallthrough(join));
3550 let fail_exit = match if_false {
3551 Some(s) => self.walk_stmt(s, fail)?,
3552 None => fail,
3553 };
3554 self.close(fail_exit, Terminator::Fallthrough(join));
3555 Ok(join)
3556 }
3557
3558 /// Inlines `call` (to `task`, whose body can suspend) at `cur`: a fresh
3559 /// entry block binds the formals ([`ArgBinding::In`]/[`ArgBinding::
3560 /// Init`]) and initializes the task's locals, the body is walked in
3561 /// place (a `return;` jumps to the continuation), and the continuation
3562 /// block starts with the output copy-backs ([`ArgBinding::Out`]).
3563 fn inline_task_call(
3564 &mut self,
3565 call: Expression<'d>,
3566 task: Symbol<'d>,
3567 cur: BlockId,
3568 ) -> Result<BlockId, LoweringError> {
3569 let result_temp = self.pending_result_temp.take();
3570 // Through a virtual interface (`vif.t(...)`): the task of the
3571 // instance it selects -- one inlined copy per candidate instance
3572 // (`netlist::vif`), picked by its id; `null` runs nothing.
3573 if let Some((vif, targets)) = crate::netlist::vif::call_targets(&call) {
3574 let actuals: Vec<Expression<'d>> = call.operands()[1..].to_vec();
3575 let join = self.alloc_block();
3576 let mut arms = Vec::new();
3577 for target in targets {
3578 let arm = self.alloc_block();
3579 arms.push(arm);
3580 match target {
3581 Some(t) => {
3582 let exit = self.inline_task_body(&actuals, t, arm, None)?;
3583 self.park_call_result(exit, result_temp, t);
3584 self.close(exit, Terminator::Fallthrough(join));
3585 }
3586 None => self.close(arm, Terminator::Fallthrough(join)),
3587 }
3588 }
3589 self.close(
3590 cur,
3591 Terminator::Switch {
3592 on: SwitchOn::VifId(vif),
3593 arms,
3594 default_blk: join,
3595 report: None,
3596 },
3597 );
3598 return Ok(join);
3599 }
3600 // A class-method task (LRM 8.6): its `this` is bound to the receiver
3601 // (`obj.run()`), or for an implicit-`this` call (`run()` inside
3602 // another method) to the caller's current object, and kept in a frame
3603 // local across the suspend points.
3604 self.call_anchor = Some(call);
3605 let receiver = call.call_this_class();
3606 let mut actuals = call.operands();
3607 if receiver.is_some() && !actuals.is_empty() {
3608 actuals.remove(0);
3609 }
3610 // A `pure virtual` task has no body and so no `this` of its own; its
3611 // calls still dispatch over the implementations below (LRM 8.21).
3612 let pure = task.subroutine_flags().contains(sv_lang::MethodFlags::PURE);
3613 let this_var = task.subroutine_this_var();
3614 if this_var.is_none() && !pure {
3615 let exit = self.inline_task_body(&actuals, task, cur, None)?;
3616 self.park_call_result(exit, result_temp, task);
3617 return Ok(exit);
3618 }
3619 // A virtual task is bound by the object's class at run time (LRM
3620 // 8.20): one inlined override per implementation the possible dynamic
3621 // classes resolve it to, picked by the object's class. A call through
3622 // `super.` is not virtual.
3623 if !(receiver.is_none() && is_explicit_super(call))
3624 && (task
3625 .subroutine_flags()
3626 .contains(sv_lang::MethodFlags::VIRTUAL)
3627 || task.subroutine_override().is_some())
3628 {
3629 let impls = self
3630 .virtual_tasks
3631 .as_ref()
3632 .map(|v| v.implementations(&task))
3633 .unwrap_or_default();
3634 // No class the design creates implements it: no object can
3635 // reach the call, so it can only run on a null handle (an
3636 // error at run time, as for any call through `null`). E.g. UVM's
3637 // `uvm_sequencer_base::start_phase_sequence` in a bench with no
3638 // sequencer.
3639 let join = self.alloc_block();
3640 let mut arms = Vec::new();
3641 let mut ids = Vec::new();
3642 for (class_ids, imp) in impls {
3643 let this_var = imp.subroutine_this_var().ok_or_else(|| {
3644 LoweringError::malformed(format!(
3645 "override of virtual task `{}` has no `this`",
3646 task.name()
3647 ))
3648 })?;
3649 let arm = self.alloc_block();
3650 arms.push(arm);
3651 ids.push(class_ids);
3652 let exit = self.inline_task_body(&actuals, imp, arm, Some((this_var, receiver)))?;
3653 self.park_call_result(exit, result_temp, imp);
3654 self.close(exit, Terminator::Fallthrough(join));
3655 }
3656 self.close(
3657 cur,
3658 Terminator::Switch {
3659 on: SwitchOn::ObjClass { receiver, ids },
3660 arms,
3661 default_blk: join,
3662 // A null (or dead) handle runs no override: an error.
3663 report: Some(call.source_range().start),
3664 },
3665 );
3666 return Ok(join);
3667 }
3668 let this_var = this_var.ok_or_else(|| {
3669 LoweringError::malformed(format!("method task `{}` has no `this`", task.name()))
3670 })?;
3671 let exit = self.inline_task_body(&actuals, task, cur, Some((this_var, receiver)))?;
3672 self.park_call_result(exit, result_temp, task);
3673 Ok(exit)
3674 }
3675
3676 /// At the end of an inlined function `imp` (block `exit`): copies its
3677 /// result variable into the call's capture temporary, when it has one.
3678 fn park_call_result(
3679 &mut self,
3680 exit: BlockId,
3681 result_temp: Option<(u32, Expression<'d>)>,
3682 imp: Symbol<'d>,
3683 ) {
3684 if let Some((temp, _)) = result_temp
3685 && let Some(rv) = imp.subroutine_return_val_var()
3686 {
3687 self.blocks[exit.0 as usize]
3688 .bindings
3689 .push(ArgBinding::ToCaptured { temp, formal: rv });
3690 }
3691 }
3692
3693 /// The first call of a task that calls itself (directly or through other
3694 /// tasks) and contains timing controls (LRM 1800-2023 §13.4, §13.4.1):
3695 /// inlining cannot bound the depth, so each activation is a forked child
3696 /// of its caller that the caller joins. The caller evaluates the
3697 /// arguments now ([`ArgBinding::Capture`]), the child copies them into
3698 /// its own formals ([`ArgBinding::FromCaptured`]) and runs the body in a
3699 /// frame of its own (automatic variables, §6.21); a call inside the body
3700 /// forks the same kernel again ([`Self::recursive_self_call`]).
3701 ///
3702 /// `output` and `inout` formals (LRM 1800-2023 13.5) are copied out when
3703 /// the activation ends: parked in a temporary ([`ArgBinding::ToCaptured`]),
3704 /// handed to the spawner's own temporary ([`InheritedSlot::Return`]) and
3705 /// assigned to the actual after the join ([`ArgBinding::AssignCaptured`]).
3706 /// They are limited to what a capture temporary holds (a packed value of
3707 /// at most 128 bits).
3708 ///
3709 /// A class method (LRM 1800-2023 8.11) also takes its `this` as capture 0
3710 /// (an implicit receiver by [`ArgBinding::CaptureThis`]). A recursive
3711 /// void function that spawns processes is handled the same way; its
3712 /// `join_none` children may then start earlier than in a call that never
3713 /// yields (the caller waits at each recursive join).
3714 ///
3715 /// Deviation: the activation is a fork child, so `disable fork` and
3716 /// `wait fork` of the caller see it; only tasks with automatic lifetime
3717 /// are supported (`ref` / static formals and a `disable` of the task fail
3718 /// by name).
3719 fn inline_recursive_task(
3720 &mut self,
3721 actuals: &[Expression<'d>],
3722 task: Symbol<'d>,
3723 cur: BlockId,
3724 this: Option<(Symbol<'d>, Option<Expression<'d>>)>,
3725 ) -> Result<BlockId, LoweringError> {
3726 let refuse = |why: &str| {
3727 LoweringError::unsupported(format!(
3728 "recursive call to task `{}`, which contains timing controls: {why} \
3729 (LRM 1800-2023 13.4.1)",
3730 task.name()
3731 ))
3732 };
3733 let body = proc_body(&task).ok_or_else(|| {
3734 LoweringError::malformed(format!("task `{}` has no body", task.name()))
3735 })?;
3736 let mut formals: Vec<Symbol<'d>> = task.subroutine_arguments().collect();
3737 if formals.len() != actuals.len() {
3738 return Err(LoweringError::malformed(format!(
3739 "call to task `{}` has {} arguments for {} formals",
3740 task.name(),
3741 actuals.len(),
3742 formals.len()
3743 )));
3744 }
3745 if task.subroutine_kind() == Some(sv_lang::SubroutineKind::Function)
3746 && task.subroutine_return_type().is_some_and(|t| !t.is_void())
3747 {
3748 return Err(refuse("a value-returning function is not supported"));
3749 }
3750 for f in &formals {
3751 if crate::netlist::is_static_procedural_var(f) {
3752 return Err(refuse("a recursive task must be `automatic`"));
3753 }
3754 }
3755 // slang binds an output/inout actual as `actual = <formal>`: the
3756 // actual is its target.
3757 let mut actuals: Vec<Expression<'d>> = actuals
3758 .iter()
3759 .zip(&formals)
3760 .map(|(&a, f)| {
3761 if f.formal_argument_direction() == sv_lang::ArgumentDirection::In {
3762 a
3763 } else {
3764 call_actual_lvalue(a)
3765 }
3766 })
3767 .collect();
3768 // A class method: its `this` is formal 0, an input handle.
3769 let has_this = this.is_some();
3770 let mut implicit_this = false;
3771 if let Some((this_var, recv)) = this {
3772 let anchor = self.call_anchor.ok_or_else(|| {
3773 LoweringError::malformed("recursive method call without a call expression")
3774 })?;
3775 implicit_this = recv.is_none();
3776 formals.insert(0, this_var);
3777 actuals.insert(0, recv.unwrap_or(anchor));
3778 }
3779 let n = formals.len() as u32;
3780 let actuals = &actuals[..];
3781 // A `ref` formal bound to static storage (a variable or a net, directly or
3782 // through an enclosing call's aliased `ref`) is that storage in every
3783 // activation (LRM 1800-2023 13.5.2): no capture, no copy-back. Every
3784 // recursive call must bind it to the same storage
3785 // ([`Self::recursive_self_call`]); anything else is refused by name.
3786 let mut ref_alias: Vec<Option<Symbol<'d>>> = vec![None; formals.len()];
3787 for (i, f) in formals.iter().enumerate() {
3788 if f.formal_argument_direction() != sv_lang::ArgumentDirection::Ref {
3789 continue;
3790 }
3791 let is_event = f
3792 .value_type()
3793 .as_ref()
3794 .is_some_and(|t| crate::state::event_pool::event_count(t).is_some());
3795 match self.ref_storage(actuals[i]) {
3796 Some(t) if !is_event => ref_alias[i] = Some(t),
3797 _ => {
3798 return Err(refuse(
3799 "a `ref` argument must name a static variable or net (or an \
3800 enclosing task's aliased `ref` formal)",
3801 ));
3802 }
3803 }
3804 }
3805 let is_in = |i: usize| {
3806 (has_this && i == 0)
3807 || formals[i].formal_argument_direction() == sv_lang::ArgumentDirection::In
3808 || ref_alias[i].is_some()
3809 };
3810 let outs: Vec<usize> = (0..formals.len()).filter(|&i| !is_in(i)).collect();
3811 let groups = if outs.is_empty() { 2 } else { 4 };
3812
3813 // The branch: its own builder. Capture temporaries `0..n` hold the
3814 // incoming arguments, `n..2n` those of a recursive call; with
3815 // `output`/`inout` formals `2n..3n` hold this activation's results
3816 // and `3n..4n` those of a recursive call.
3817 // The same task activated from the same context builds the same
3818 // Root: build it once per process ([`RootKey`]) and the lowering
3819 // compiles it once ([`ProcessCfg::share_id`]); only the call site's
3820 // bindings below are per site. Not under a clock inference.
3821 let key = (self.global_clock.is_none() && self.checker_clock.is_none()).then(|| {
3822 MemoKey::Root(RootKey {
3823 task: task.uid(),
3824 this_var: this.map(|(t, _)| t.uid()),
3825 default_clocking: self.default_clocking.map(|c| c.uid()),
3826 rec_outer: self.rec_outer.clone(),
3827 chain: self
3828 .rec_up
3829 .iter()
3830 .chain(self.rec.iter())
3831 .map(|r| r.share)
3832 .collect(),
3833 ref_aliases: self
3834 .ref_stack
3835 .iter()
3836 .map(|(f, t)| (f.uid(), t.uid()))
3837 .chain(
3838 formals
3839 .iter()
3840 .zip(&ref_alias)
3841 .filter_map(|(f, t)| t.map(|t| (f.uid(), t.uid()))),
3842 )
3843 .collect(),
3844 labels: self.fork_branch_names.clone(),
3845 ancestors: self.ancestor_blocks.clone(),
3846 })
3847 });
3848 let cached = key
3849 .as_ref()
3850 .and_then(|k| self.fork_memo.borrow().get(k).cloned());
3851 let mut branch = if let Some(hit) = cached {
3852 hit
3853 } else {
3854 let share = NEXT_SHARE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3855 let mut b = CfgBuilder::new();
3856 b.fork_memo = self.fork_memo.clone();
3857 b.global_clock = self.global_clock;
3858 b.virtual_tasks = self.virtual_tasks.clone();
3859 b.fork_branch_names = self.fork_branch_names.clone();
3860 b.ancestor_blocks = self.ancestor_blocks.clone();
3861 b.default_clocking = self.default_clocking;
3862 b.checker_clock = self.checker_clock;
3863 b.rec_outer = self.rec_outer.clone();
3864 b.rec_outer.push(task.uid());
3865 b.rec_up = self.rec_up.clone();
3866 b.rec_up.extend(self.rec.clone());
3867 b.rec = Some(RecursiveTask {
3868 task: task.uid(),
3869 share,
3870 proto: actuals
3871 .iter()
3872 .copied()
3873 .zip(formals.iter().copied())
3874 .collect(),
3875 n,
3876 groups,
3877 outs: (0..formals.len()).map(|i| !is_in(i)).collect(),
3878 refs: ref_alias.clone(),
3879 has_this,
3880 body,
3881 });
3882 b.ref_stack = self.ref_stack.clone();
3883 for (f, t) in formals.iter().zip(&ref_alias) {
3884 if let Some(t) = t {
3885 b.ref_stack.push((*f, *t));
3886 b.ref_candidates.push((*f, Some(*t)));
3887 }
3888 }
3889 if let Some((this_var, _)) = this {
3890 b.this_stack.push(this_var);
3891 }
3892 for _ in 0..groups {
3893 for (f, &a) in formals.iter().zip(actuals) {
3894 b.set_capture_type(a, *f);
3895 }
3896 }
3897 let entry = b.alloc_block();
3898 for (i, f) in formals.iter().enumerate() {
3899 // An `output` formal starts out as its type's default.
3900 if ref_alias[i].is_some() {
3901 continue;
3902 }
3903 b.blocks[entry.0 as usize].bindings.push(
3904 if !is_in(i) && f.formal_argument_direction() == sv_lang::ArgumentDirection::Out
3905 {
3906 ArgBinding::Init { var: *f }
3907 } else {
3908 ArgBinding::FromCaptured {
3909 temp: i as u32,
3910 formal: *f,
3911 }
3912 },
3913 );
3914 b.inlined_locals.push(*f);
3915 }
3916 for local in task_locals(&task) {
3917 // A method's `this` is bound above (formal 0).
3918 if this.is_some_and(|(t, _)| t.uid() == local.uid()) {
3919 continue;
3920 }
3921 if crate::netlist::is_static_procedural_var(&local) {
3922 continue;
3923 }
3924 if !is_automatic_block_var(&local) {
3925 b.blocks[entry.0 as usize]
3926 .bindings
3927 .push(ArgBinding::Init { var: local });
3928 }
3929 b.inlined_locals.push(local);
3930 }
3931 for v in crate::patterns::pattern_vars(body.as_sem_node()) {
3932 if !b.inlined_locals.iter().any(|l| l.uid() == v.uid()) {
3933 b.inlined_locals.push(v);
3934 }
3935 }
3936 let after = b.alloc_block();
3937 b.target_depth.insert(after, 0);
3938 b.return_targets.push(after);
3939 // `disable task;` ends every activation (LRM 9.6.2): each one's exit
3940 // is the end of its own kernel.
3941 let task_name = task.name().to_string();
3942 let task_scoped = is_disabled_somewhere(&task_name);
3943 if task_scoped {
3944 b.scope_chain
3945 .push((task_name.clone(), after, b.frame_stack.len()));
3946 b.disable_targets
3947 .push((task_name, after, b.frame_stack.len()));
3948 }
3949 // LRM 19.3 / 19.5.5: every activation signals the task's `begin` at
3950 // its first statement and `end` at its exit block (also reached by a
3951 // `disable task;`), like an inlined call.
3952 b.block_event(entry, task, false);
3953 b.event_stack.push((task, b.disable_targets.len()));
3954 let exit = b.walk_stmt(body, entry);
3955 b.event_stack.pop();
3956 if task_scoped {
3957 b.scope_chain.pop();
3958 b.disable_targets.pop();
3959 }
3960 let exit = exit?;
3961 b.close(exit, Terminator::Fallthrough(after));
3962 b.block_event(after, task, true);
3963 for &i in &outs {
3964 b.blocks[after.0 as usize]
3965 .bindings
3966 .push(ArgBinding::ToCaptured {
3967 temp: 2 * n + i as u32,
3968 formal: formals[i],
3969 });
3970 }
3971 b.close(after, Terminator::End { loop_back: None });
3972 let inlined_locals = std::mem::take(&mut b.inlined_locals);
3973 let captures = std::mem::take(&mut b.captures);
3974 let capture_types = std::mem::take(&mut b.capture_types);
3975 let b_ref_formals: Vec<Symbol<'d>> = b.ref_stack.iter().map(|&(f, _)| f).collect();
3976 let ref_bindings = b.take_ref_bindings();
3977 let disable_scopes = b.take_disable_scopes();
3978 let disable_broadcast = std::mem::take(&mut b.disable_broadcast);
3979 let fork_branch_names = std::mem::take(&mut b.fork_branch_names);
3980 let mut built = ProcessCfg {
3981 blocks: b.finish(),
3982 entry,
3983 body,
3984 inlined_locals,
3985 captures,
3986 captured: Vec::new(),
3987 ref_bindings,
3988 disable_names: Vec::new(),
3989 fork_branch_names,
3990 disable_scopes,
3991 disable_broadcast,
3992 inherited: Vec::new(),
3993 capture_types,
3994 recursion: Recursion::Root,
3995 share_id: None,
3996 };
3997 built.captured = captured_vars(&built);
3998 built
3999 .captured
4000 .retain(|c| !b_ref_formals.iter().any(|f| f.uid() == c.uid()));
4001 if let Some(k) = key {
4002 built.share_id = Some(share);
4003 self.fork_memo.borrow_mut().insert(k, built.clone());
4004 }
4005 built
4006 };
4007
4008 // The caller: the arguments, evaluated now, into temporaries the
4009 // child inherits by value.
4010 let call = self.alloc_block();
4011 self.close(cur, Terminator::Fallthrough(call));
4012 for (i, (f, &a)) in formals.iter().zip(actuals).enumerate() {
4013 let temp = self.set_capture_type(a, *f);
4014 self.blocks[call.0 as usize]
4015 .bindings
4016 .push(if i == 0 && implicit_this {
4017 ArgBinding::CaptureThis { temp }
4018 } else {
4019 ArgBinding::Capture { temp, value: a }
4020 });
4021 branch.inherited.push(InheritedSlot::Temp {
4022 parent: temp,
4023 child: i as u32,
4024 });
4025 }
4026 let join_blk = self.alloc_block();
4027 // The results, back into the actuals once the activation is done.
4028 for &i in &outs {
4029 let ret = self.set_capture_type(actuals[i], formals[i]);
4030 branch.inherited.push(InheritedSlot::Return {
4031 parent: ret,
4032 child: 2 * n + i as u32,
4033 });
4034 self.blocks[join_blk.0 as usize]
4035 .bindings
4036 .push(ArgBinding::AssignCaptured {
4037 temp: ret,
4038 target: actuals[i],
4039 nba: false,
4040 });
4041 }
4042 self.close(
4043 call,
4044 Terminator::ForkJoin {
4045 kind: StatementBlockKind::JoinAll,
4046 branches: vec![branch],
4047 join_blk,
4048 },
4049 );
4050 Ok(join_blk)
4051 }
4052
4053 /// A call of the recursive task inside its own activation
4054 /// ([`Self::inline_recursive_task`]): the arguments go to capture
4055 /// temporaries `n..2n`, and a placeholder branch forks the activation's
4056 /// own kernel again ([`Recursion::SelfSpawn`]), joined at once.
4057 fn recursive_self_call(
4058 &mut self,
4059 actuals: &[Expression<'d>],
4060 cur: BlockId,
4061 recv: Option<Option<Expression<'d>>>,
4062 ) -> Result<BlockId, LoweringError> {
4063 let rec = self
4064 .rec
4065 .as_ref()
4066 .ok_or_else(|| LoweringError::malformed("recursive call outside its activation"))?;
4067 let (n, body, rec_outs) = (rec.n, rec.body, rec.outs.clone());
4068 let skip = usize::from(rec.has_this);
4069 for (a, t) in actuals.iter().zip(&rec.refs[skip..]) {
4070 if let Some(t) = t
4071 && self.ref_storage(call_actual_lvalue(*a)).map(|s| s.uid()) != Some(t.uid())
4072 {
4073 return Err(LoweringError::unsupported(
4074 "recursive call whose `ref` argument is not the storage the first call \
4075 bound (a recursive task's `ref` formal is an alias of one static \
4076 variable or net; LRM 1800-2023 13.5.2)",
4077 ));
4078 }
4079 }
4080 let mut actuals: Vec<Expression<'d>> = actuals
4081 .iter()
4082 .zip(&rec_outs[skip..])
4083 .map(|(&a, &out)| if out { call_actual_lvalue(a) } else { a })
4084 .collect();
4085 // The receiver of a method call is capture 0.
4086 let mut implicit_this = false;
4087 if let Some(r) = recv {
4088 let anchor = self.call_anchor.ok_or_else(|| {
4089 LoweringError::malformed("recursive method call without a call expression")
4090 })?;
4091 implicit_this = r.is_none();
4092 actuals.insert(0, r.unwrap_or(anchor));
4093 }
4094 let actuals = &actuals[..];
4095 if actuals.len() as u32 != n {
4096 return Err(LoweringError::malformed(
4097 "recursive call with the wrong number of arguments",
4098 ));
4099 }
4100 let call = self.alloc_block();
4101 self.close(cur, Terminator::Fallthrough(call));
4102 let mut inherited = Vec::new();
4103 for (i, &a) in actuals.iter().enumerate() {
4104 let temp = n + i as u32;
4105 self.blocks[call.0 as usize]
4106 .bindings
4107 .push(if i == 0 && implicit_this {
4108 ArgBinding::CaptureThis { temp }
4109 } else {
4110 ArgBinding::Capture { temp, value: a }
4111 });
4112 inherited.push(InheritedSlot::Temp {
4113 parent: temp,
4114 child: i as u32,
4115 });
4116 }
4117 // The results of the callee, back into the actuals after the join.
4118 let join_blk = self.alloc_block();
4119 for (i, &a) in actuals.iter().enumerate() {
4120 if rec_outs[i] {
4121 let ret = 3 * n + i as u32;
4122 inherited.push(InheritedSlot::Return {
4123 parent: ret,
4124 child: 2 * n + i as u32,
4125 });
4126 self.blocks[join_blk.0 as usize]
4127 .bindings
4128 .push(ArgBinding::AssignCaptured {
4129 temp: ret,
4130 target: a,
4131 nba: false,
4132 });
4133 }
4134 }
4135 let mut stub = CfgBuilder::new();
4136 let entry = stub.alloc_block();
4137 stub.close(entry, Terminator::End { loop_back: None });
4138 let branch = ProcessCfg {
4139 blocks: stub.finish(),
4140 entry,
4141 body,
4142 inlined_locals: Vec::new(),
4143 captures: Vec::new(),
4144 captured: Vec::new(),
4145 ref_bindings: Vec::new(),
4146 disable_names: Vec::new(),
4147 fork_branch_names: Vec::new(),
4148 disable_scopes: Vec::new(),
4149 disable_broadcast: Vec::new(),
4150 inherited,
4151 capture_types: Vec::new(),
4152 recursion: Recursion::SelfSpawn,
4153 share_id: None,
4154 };
4155 self.close(
4156 call,
4157 Terminator::ForkJoin {
4158 kind: StatementBlockKind::JoinAll,
4159 branches: vec![branch],
4160 join_blk,
4161 },
4162 );
4163 Ok(join_blk)
4164 }
4165
4166 /// A call, inside one activation, of the task of the enclosing activation
4167 /// `self.rec_up[k]` (mutual recursion, LRM 1800-2023 13.4.1): the
4168 /// arguments go to fresh capture temporaries and a placeholder branch
4169 /// forks that activation's kernel again ([`Recursion::AncestorSpawn`]),
4170 /// joined at once.
4171 fn recursive_ancestor_call(
4172 &mut self,
4173 k: usize,
4174 actuals: &[Expression<'d>],
4175 cur: BlockId,
4176 recv: Option<Option<Expression<'d>>>,
4177 ) -> Result<BlockId, LoweringError> {
4178 let anc = self.rec_up[k].clone();
4179 if anc.refs.iter().any(Option::is_some) {
4180 return Err(LoweringError::unsupported(
4181 "mutually recursive tasks with a `ref` formal (LRM 1800-2023 13.4.1, 13.5.2)",
4182 ));
4183 }
4184 let skip = usize::from(anc.has_this);
4185 let mut actuals: Vec<Expression<'d>> = actuals
4186 .iter()
4187 .zip(&anc.outs[skip..])
4188 .map(|(&a, &out)| if out { call_actual_lvalue(a) } else { a })
4189 .collect();
4190 let mut implicit_this = false;
4191 if let Some(r) = recv {
4192 let anchor = self.call_anchor.ok_or_else(|| {
4193 LoweringError::malformed("recursive method call without a call expression")
4194 })?;
4195 implicit_this = r.is_none();
4196 actuals.insert(0, r.unwrap_or(anchor));
4197 }
4198 if actuals.len() as u32 != anc.n {
4199 return Err(LoweringError::malformed(
4200 "recursive call with the wrong number of arguments",
4201 ));
4202 }
4203 let call = self.alloc_block();
4204 self.close(cur, Terminator::Fallthrough(call));
4205 let mut inherited = Vec::new();
4206 for (i, &a) in actuals.iter().enumerate() {
4207 let temp = self.set_capture_type(a, anc.proto[i].1);
4208 self.blocks[call.0 as usize]
4209 .bindings
4210 .push(if i == 0 && implicit_this {
4211 ArgBinding::CaptureThis { temp }
4212 } else {
4213 ArgBinding::Capture { temp, value: a }
4214 });
4215 inherited.push(InheritedSlot::Temp {
4216 parent: temp,
4217 child: i as u32,
4218 });
4219 }
4220 // The ancestor's results, back into the actuals after the join.
4221 let join_blk = self.alloc_block();
4222 for (i, &a) in actuals.iter().enumerate() {
4223 if anc.outs[i] {
4224 let ret = self.set_capture_type(a, anc.proto[i].1);
4225 inherited.push(InheritedSlot::Return {
4226 parent: ret,
4227 child: 2 * anc.n + i as u32,
4228 });
4229 self.blocks[join_blk.0 as usize]
4230 .bindings
4231 .push(ArgBinding::AssignCaptured {
4232 temp: ret,
4233 target: a,
4234 nba: false,
4235 });
4236 }
4237 }
4238 let mut stub = CfgBuilder::new();
4239 let entry = stub.alloc_block();
4240 stub.close(entry, Terminator::End { loop_back: None });
4241 let branch = ProcessCfg {
4242 blocks: stub.finish(),
4243 entry,
4244 body: anc.body,
4245 inlined_locals: Vec::new(),
4246 captures: Vec::new(),
4247 captured: Vec::new(),
4248 ref_bindings: Vec::new(),
4249 disable_names: Vec::new(),
4250 fork_branch_names: Vec::new(),
4251 disable_scopes: Vec::new(),
4252 disable_broadcast: Vec::new(),
4253 inherited,
4254 capture_types: Vec::new(),
4255 recursion: Recursion::AncestorSpawn((self.rec_up.len() - k) as u32),
4256 share_id: None,
4257 };
4258 self.close(
4259 call,
4260 Terminator::ForkJoin {
4261 kind: StatementBlockKind::JoinAll,
4262 branches: vec![branch],
4263 join_blk,
4264 },
4265 );
4266 Ok(join_blk)
4267 }
4268
4269 /// A new capture temporary shaped like `formal`, holding `value`.
4270 fn set_capture_type(&mut self, value: Expression<'d>, formal: Symbol<'d>) -> u32 {
4271 let temp = self.captures.len() as u32;
4272 self.captures.push(value);
4273 self.capture_types.resize(temp as usize, None);
4274 self.capture_types.push(Some(formal));
4275 temp
4276 }
4277
4278 /// [`Self::inline_task_call`] of `task` with the arguments `actuals`.
4279 /// An expression statement that calls a value-returning function whose
4280 /// body spawns processes (`fork ... join_none` in a function, LRM
4281 /// 1800-2023 13.4.4), which cannot be compiled: each such call is inlined
4282 /// ahead of the statement (arguments first, in source order) and the
4283 /// statement reads the function's result variable instead of calling it
4284 /// (codegen `lower_user_subroutine_call_of`). `None` when the statement
4285 /// has no such call. The same function twice in one statement, or a call
4286 /// to a virtual method, is refused by name.
4287 fn hoist_suspending_function_calls(
4288 &mut self,
4289 stmt: Statement<'d>,
4290 cur: BlockId,
4291 ) -> Result<Option<BlockId>, LoweringError> {
4292 let Some(cur) = self.hoist_suspending_calls_in(stmt.as_sem_node(), cur)? else {
4293 return Ok(None);
4294 };
4295 self.push(cur, stmt);
4296 Ok(Some(cur))
4297 }
4298
4299 /// [`Self::hoist_suspending_function_calls`]' inlining half over any
4300 /// expression tree: inlines every spawning function call under `node`
4301 /// ahead of the caller, which then reads their result variables. Also
4302 /// used for an inlined function's `return expr;` (`return m(...)`).
4303 fn hoist_suspending_calls_in(
4304 &mut self,
4305 node: SemNode<'d>,
4306 cur: BlockId,
4307 ) -> Result<Option<BlockId>, LoweringError> {
4308 let mut calls = Vec::new();
4309 collect_suspending_function_calls(node, &mut calls);
4310 if calls.is_empty() {
4311 return Ok(None);
4312 }
4313 let mut count: std::collections::HashMap<_, usize> = std::collections::HashMap::new();
4314 for call in &calls {
4315 if let Some(callee) = user_task_callee(call) {
4316 *count.entry(callee.uid()).or_default() += 1;
4317 }
4318 }
4319 let mut cur = cur;
4320 for call in calls {
4321 let Some(callee) = user_task_callee(&call) else {
4322 continue;
4323 };
4324 // A method (LRM 8.6) is inlined with its `this` bound to the
4325 // receiver (`inline_task_call`). A virtual one picks among
4326 // several overrides, each with a result variable of its own.
4327 let is_method =
4328 callee.subroutine_this_var().is_some() || call.call_this_class().is_some();
4329 let flags = callee.subroutine_flags();
4330 let is_virtual = is_method
4331 && (flags.contains(sv_lang::MethodFlags::VIRTUAL)
4332 || flags.contains(sv_lang::MethodFlags::PURE)
4333 || callee.subroutine_override().is_some());
4334 // The function's result variable is shared by its calls (and a
4335 // virtual method's overrides have one each): a call that cannot
4336 // read it straight after its inlining parks the value in a
4337 // temporary of its own, which the call's expression reads.
4338 let temp = (is_virtual || count.get(&callee.uid()).copied().unwrap_or(0) > 1)
4339 .then(|| {
4340 callee
4341 .subroutine_return_val_var()
4342 .map(|rv| self.set_capture_type(call, rv))
4343 })
4344 .flatten();
4345 if let Some(temp) = temp {
4346 self.pending_result_temp = Some((temp, call));
4347 }
4348 cur = if is_method {
4349 self.inline_task_call(call, callee, cur)?
4350 } else {
4351 let actuals = call.operands();
4352 let exit = self.inline_task_body(&actuals, callee, cur, None)?;
4353 self.park_call_result(exit, temp.map(|t| (t, call)), callee);
4354 exit
4355 };
4356 self.pending_result_temp = None;
4357 if let Some(temp) = temp {
4358 self.blocks[cur.0 as usize]
4359 .bindings
4360 .push(ArgBinding::CallResult { temp, call });
4361 }
4362 }
4363 Ok(Some(cur))
4364 }
4365
4366 fn inline_task_body(
4367 &mut self,
4368 actuals: &[Expression<'d>],
4369 task: Symbol<'d>,
4370 cur: BlockId,
4371 this: Option<(Symbol<'d>, Option<Expression<'d>>)>,
4372 ) -> Result<BlockId, LoweringError> {
4373 if let Some(rec) = &self.rec
4374 && rec.task == task.uid()
4375 && this.is_some() == rec.has_this
4376 {
4377 return self.recursive_self_call(actuals, cur, this.map(|t| t.1));
4378 }
4379 if let Some(k) = self
4380 .rec_up
4381 .iter()
4382 .rposition(|r| r.task == task.uid() && r.has_this == this.is_some())
4383 {
4384 return self.recursive_ancestor_call(k, actuals, cur, this.map(|t| t.1));
4385 }
4386 if self.rec.is_none() && self.rec_outer.contains(&task.uid()) {
4387 // A fork branch the activation does not hand its recursion to
4388 // (a virtual-task or detached branch): inlining would not end.
4389 return Err(LoweringError::unsupported(format!(
4390 "call to task `{}` from a branch of its own recursive activation that cannot \
4391 spawn it (LRM 1800-2023 13.4.1)",
4392 task.name()
4393 )));
4394 }
4395 if self.inline_stack.contains(&task.uid()) {
4396 return Err(LoweringError::unsupported(format!(
4397 "recursive call to task `{}`, which contains timing controls (it has to be \
4398 inlined into the calling process)",
4399 task.name()
4400 )));
4401 }
4402 if !self.rec_outer.contains(&task.uid())
4403 && task_reaches_itself(&task, self.virtual_tasks.as_deref())
4404 {
4405 return self.inline_recursive_task(actuals, task, cur, this);
4406 }
4407 let body = proc_body(&task).ok_or_else(|| {
4408 LoweringError::malformed(format!("task `{}` has no body", task.name()))
4409 })?;
4410 let formals: Vec<Symbol<'d>> = task.subroutine_arguments().collect();
4411 if formals.len() != actuals.len() {
4412 return Err(LoweringError::malformed(format!(
4413 "call to task `{}` has {} arguments for {} formals",
4414 task.name(),
4415 actuals.len(),
4416 formals.len()
4417 )));
4418 }
4419 let entry = self.alloc_block();
4420 self.close(cur, Terminator::Fallthrough(entry));
4421 let after = self.alloc_block();
4422 self.target_depth.insert(after, self.owned_scopes.len());
4423 let frame_mark = self.inlined_locals.len();
4424 let mut releases = Vec::new();
4425 if let Some((this_var, receiver)) = this {
4426 let binding = match receiver {
4427 Some(r) => ArgBinding::In {
4428 formal: this_var,
4429 actual: r,
4430 },
4431 None => ArgBinding::ThisOf { var: this_var },
4432 };
4433 self.blocks[entry.0 as usize].bindings.push(binding);
4434 self.inlined_locals.push(this_var);
4435 // Both bindings hold one count on the object for the call.
4436 releases.push(ArgBinding::Release { var: this_var });
4437 }
4438 let mut copy_backs = Vec::new();
4439 let mut aliases = Vec::new();
4440 let mut bound_refs: Vec<(Symbol<'d>, Expression<'d>)> = Vec::new();
4441 for (formal, &actual) in formals.iter().zip(actuals) {
4442 // slang binds an output/inout actual as `actual = <formal>`.
4443 let lvalue = if actual.kind() == ExpressionKind::Assignment {
4444 actual.left().ok_or_else(|| {
4445 LoweringError::malformed("output argument with no target expression")
4446 })?
4447 } else {
4448 actual
4449 };
4450 // A `ref` naming static storage (directly or through an
4451 // enclosing call's aliased `ref`) is that storage in the body.
4452 // (An `event` (or event array) ref formal is a handle: copied in and back out, like
4453 // `inout`, so the body's `r = e` merges the actual's event.)
4454 if formal.formal_argument_direction() == sv_lang::ArgumentDirection::Ref
4455 && !formal
4456 .value_type()
4457 .as_ref()
4458 .is_some_and(|t| crate::state::event_pool::event_count(t).is_some())
4459 && let Some(target) = self.ref_storage(lvalue)
4460 {
4461 self.ref_candidates.push((*formal, Some(target)));
4462 aliases.push((*formal, target));
4463 continue;
4464 }
4465 // A `ref` to a queue / dynamic-array element of a packed or real
4466 // type (LRM 1800-2023 13.5.2): the formal is a live alias, every
4467 // access of it goes through a runtime reference
4468 // ([`ArgBinding::RefOpen`]); other element types keep the
4469 // copy-in/copy-out of `inout`.
4470 if formal.formal_argument_direction() == sv_lang::ArgumentDirection::Ref
4471 && lvalue.kind() == ExpressionKind::ElementSelect
4472 && formal
4473 .value_type()
4474 .is_some_and(|t| t.is_integral() || t.is_floating())
4475 && lvalue
4476 .select_value()
4477 .and_then(|b| b.expr_type())
4478 .is_some_and(|t| t.is_dynamically_sized_array() || t.is_associative_array())
4479 {
4480 // One reference-id slot per formal, shared by every call
4481 // site: the body reaches the reference through the formal.
4482 let prior =
4483 self.blocks
4484 .iter()
4485 .flat_map(|b| b.bindings.iter())
4486 .find_map(|b| match b {
4487 ArgBinding::RefOpen {
4488 temp, formal: f, ..
4489 } if f.uid() == formal.uid() => Some(*temp),
4490 _ => None,
4491 });
4492 let temp = prior.unwrap_or_else(|| {
4493 self.captures.push(lvalue);
4494 self.captures.len() as u32 - 1
4495 });
4496 self.blocks[entry.0 as usize]
4497 .bindings
4498 .push(ArgBinding::RefOpen {
4499 temp,
4500 formal: *formal,
4501 actual: lvalue,
4502 });
4503 copy_backs.push(ArgBinding::RefClose {
4504 temp,
4505 formal: *formal,
4506 actual: lvalue,
4507 });
4508 self.ref_candidates.push((*formal, None));
4509 if !crate::netlist::is_static_procedural_var(formal) {
4510 self.inlined_locals.push(*formal);
4511 }
4512 continue;
4513 }
4514 let binding = match formal.formal_argument_direction() {
4515 sv_lang::ArgumentDirection::In => ArgBinding::In {
4516 formal: *formal,
4517 actual,
4518 },
4519 sv_lang::ArgumentDirection::Out => ArgBinding::Init { var: *formal },
4520 sv_lang::ArgumentDirection::InOut | sv_lang::ArgumentDirection::Ref => {
4521 ArgBinding::In {
4522 formal: *formal,
4523 actual: lvalue,
4524 }
4525 }
4526 };
4527 self.blocks[entry.0 as usize].bindings.push(binding);
4528 if formal.formal_argument_direction() == sv_lang::ArgumentDirection::Ref {
4529 bound_refs.push((*formal, lvalue));
4530 let named = (crate::symid::is_value_name(lvalue.kind())
4531 && !formal
4532 .value_type()
4533 .as_ref()
4534 .is_some_and(|t| crate::state::event_pool::event_count(t).is_some()))
4535 .then(|| lvalue.referenced_symbol())
4536 .flatten();
4537 self.ref_candidates.push((*formal, named));
4538 }
4539 if formal.formal_argument_direction() != sv_lang::ArgumentDirection::In {
4540 copy_backs.push(ArgBinding::Out {
4541 actual: lvalue,
4542 formal: *formal,
4543 });
4544 }
4545 // A static task's formal is the instance's (a signal).
4546 if !crate::netlist::is_static_procedural_var(formal) {
4547 self.inlined_locals.push(*formal);
4548 if formal.formal_argument_direction() == sv_lang::ArgumentDirection::In {
4549 releases.push(ArgBinding::Release { var: *formal });
4550 }
4551 }
4552 }
4553 // A method's body (and its locals' initializers) runs against its
4554 // own `this`, in blocks of its own; the bindings above ran in the
4555 // caller's.
4556 if let Some((this_var, _)) = this {
4557 self.this_stack.push(this_var);
4558 }
4559 let n_aliases = aliases.len();
4560 self.ref_stack.extend(aliases);
4561 let body_entry = if this.is_some() || n_aliases > 0 {
4562 let b = self.alloc_block();
4563 self.close(entry, Terminator::Fallthrough(b));
4564 b
4565 } else {
4566 entry
4567 };
4568 for local in task_locals(&task) {
4569 // A method's `this` is bound above, never a default-initialized
4570 // local.
4571 if this.is_some_and(|(t, _)| t.uid() == local.uid()) {
4572 continue;
4573 }
4574 // A static variable is the instance's (a signal); a block's
4575 // automatic variables are initialized on entry to it.
4576 if crate::netlist::is_static_procedural_var(&local) {
4577 continue;
4578 }
4579 if !is_automatic_block_var(&local) {
4580 self.blocks[body_entry.0 as usize]
4581 .bindings
4582 .push(ArgBinding::Init { var: local });
4583 releases.push(ArgBinding::Release { var: local });
4584 }
4585 self.inlined_locals.push(local);
4586 }
4587 // An inlined value-returning function (a `fork` in its body cannot be
4588 // compiled, LRM 13.4.4): its result variable is a local of the
4589 // process, which `return expr;` / `f = expr;` assign and the hoisted
4590 // call's expression reads (`hoist_suspending_function_calls`).
4591 let ret_var = if task.subroutine_kind() == Some(sv_lang::SubroutineKind::Function)
4592 && task.subroutine_return_type().is_some_and(|t| !t.is_void())
4593 {
4594 task.subroutine_return_val_var()
4595 } else {
4596 None
4597 };
4598 if let Some(rv) = ret_var {
4599 self.blocks[body_entry.0 as usize]
4600 .bindings
4601 .push(ArgBinding::Init { var: rv });
4602 self.inlined_locals.push(rv);
4603 }
4604 // The pattern variables of the body's `matches` (LRM 12.6).
4605 for v in crate::patterns::pattern_vars(body.as_sem_node()) {
4606 if !self.inlined_locals.iter().any(|l| l.uid() == v.uid()) {
4607 self.inlined_locals.push(v);
4608 }
4609 }
4610 // VPI frame (37.43): the formals and locals this inlining made
4611 // automatic variables of the process, in that order.
4612 let frame_vars: Vec<Symbol<'d>> = self.inlined_locals[frame_mark..]
4613 .iter()
4614 .copied()
4615 .filter(|v| this.is_none_or(|(t, _)| t.uid() != v.uid()))
4616 .filter(|v| !bound_refs.iter().any(|(f, _)| f.uid() == v.uid()))
4617 .collect();
4618 self.frame_probe(
4619 body_entry,
4620 FrameProbe::Enter {
4621 task,
4622 vars: frame_vars.clone(),
4623 bound: bound_refs,
4624 },
4625 );
4626 self.block_event(body_entry, task, false);
4627 self.event_stack.push((task, self.disable_targets.len()));
4628 self.frame_stack.push((task, frame_vars.clone()));
4629 self.inline_stack.push(task.uid());
4630 self.return_targets.push(after);
4631 self.ret_var_stack.push(ret_var);
4632 // `disable task;` ends this activation (LRM 9.6.2), from inside it or
4633 // from another process: the exit is the block after the call, whose
4634 // frame probe ends the activation's VPI frame.
4635 let task_name = task.name().to_string();
4636 let task_scoped = is_disabled_somewhere(&task_name);
4637 let mut n_tags = 0;
4638 if task_scoped {
4639 self.scope_chain
4640 .push((task_name.clone(), after, self.frame_stack.len()));
4641 self.disable_targets
4642 .push((task_name, after, self.frame_stack.len()));
4643 // LRM 25.7.4: an `extern` method's activation is also the scope
4644 // `disable m1.a.t;` (this module instance's call) and `disable
4645 // b.t;` (every call through the interface instance) name.
4646 for tag in crate::netlist::itf::extern_scope_tags(&task) {
4647 self.scope_chain.push((tag, after, self.frame_stack.len()));
4648 n_tags += 1;
4649 }
4650 }
4651 let exit = self.walk_stmt(body, body_entry);
4652 if task_scoped {
4653 self.scope_chain.pop();
4654 self.disable_targets.pop();
4655 for _ in 0..n_tags {
4656 self.scope_chain.pop();
4657 }
4658 }
4659 self.event_stack.pop();
4660 self.return_targets.pop();
4661 self.ret_var_stack.pop();
4662 self.inline_stack.pop();
4663 self.frame_stack.pop();
4664 if this.is_some() {
4665 self.this_stack.pop();
4666 }
4667 self.ref_stack.truncate(self.ref_stack.len() - n_aliases);
4668 let exit = exit?;
4669 self.close(exit, Terminator::Fallthrough(after));
4670 self.blocks[after.0 as usize].bindings.extend(copy_backs);
4671 self.blocks[after.0 as usize].bindings.extend(releases);
4672 self.block_event(after, task, true);
4673 self.frame_probe(
4674 after,
4675 FrameProbe::Exit {
4676 task,
4677 vars: frame_vars,
4678 },
4679 );
4680 Ok(after)
4681 }
4682
4683 /// An immediate assertion whose pass or fail statement waits (LRM 16.3):
4684 /// [`Terminator::Assertion`] over the two statements' own blocks.
4685 fn walk_immediate_assertion(
4686 &mut self,
4687 stmt: Statement<'d>,
4688 (pass_stmt, fail_stmt): (Option<Statement<'d>>, Option<Statement<'d>>),
4689 cur: BlockId,
4690 ) -> Result<BlockId, LoweringError> {
4691 if stmt.is_deferred_assertion() == Some(true) {
4692 return Err(LoweringError::unsupported(
4693 "a deferred assertion (LRM 16.4) whose action block waits (the action must be \
4694 a subroutine call that is queued)"
4695 .to_string(),
4696 ));
4697 }
4698 let pass_blk = self.alloc_block();
4699 let fail_blk = self.alloc_block();
4700 let done_blk = self.alloc_block();
4701 self.close(
4702 cur,
4703 Terminator::Assertion {
4704 stmt,
4705 pass_blk,
4706 fail_blk,
4707 done_blk,
4708 },
4709 );
4710 for (action, blk) in [(pass_stmt, pass_blk), (fail_stmt, fail_blk)] {
4711 let exit = match action {
4712 Some(a) => self.walk_stmt(a, blk)?,
4713 None => blk,
4714 };
4715 self.close(exit, Terminator::Fallthrough(done_blk));
4716 }
4717 Ok(done_blk)
4718 }
4719
4720 /// A `case` whose arms need their own blocks ([`Terminator::Case`]).
4721 fn walk_case(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
4722 // A `case ... matches` has its own item and default accessors.
4723 let is_pattern = stmt.kind() == StatementKind::PatternCase;
4724 // A constant selector over arms one of which slang demoted to
4725 // `Invalid` (a statically dead arm): only the matching arm can run
4726 // (LRM 12.5), so lower just it.
4727 if !is_pattern && let Some(live) = constant_case_live_arm(&stmt) {
4728 return match live {
4729 Some(arm) => self.walk_stmt(arm, cur),
4730 None => Ok(cur),
4731 };
4732 }
4733 let arm_stmts: Vec<Statement<'d>> = if is_pattern {
4734 stmt.pattern_case_items().iter().map(|i| i.stmt()).collect()
4735 } else {
4736 stmt.case_items().iter().map(|i| i.stmt()).collect()
4737 };
4738 let arms: Vec<BlockId> = arm_stmts.iter().map(|_| self.alloc_block()).collect();
4739 let default_blk = self.alloc_block();
4740 let join = self.alloc_block();
4741 self.close(
4742 cur,
4743 Terminator::Case {
4744 stmt,
4745 arms: arms.clone(),
4746 default_blk,
4747 },
4748 );
4749 for (arm_stmt, &arm) in arm_stmts.iter().zip(&arms) {
4750 let exit = self.walk_stmt(*arm_stmt, arm)?;
4751 self.close(exit, Terminator::Fallthrough(join));
4752 }
4753 let default_stmt = if is_pattern {
4754 stmt.pattern_case_default()
4755 } else {
4756 stmt.default_case()
4757 };
4758 let default_exit = match default_stmt {
4759 Some(d) => self.walk_stmt(d, default_blk)?,
4760 None => default_blk,
4761 };
4762 self.close(default_exit, Terminator::Fallthrough(join));
4763 Ok(join)
4764 }
4765
4766 /// Walks a loop body with `continue` going to `cont` and `break` to
4767 /// `brk`.
4768 fn walk_loop_body(
4769 &mut self,
4770 body_stmt: Statement<'d>,
4771 body: BlockId,
4772 cont: BlockId,
4773 brk: BlockId,
4774 ) -> Result<BlockId, LoweringError> {
4775 self.loop_targets.push((cont, brk));
4776 let open = self.owned_scopes.len();
4777 self.target_depth.insert(cont, open);
4778 self.target_depth.insert(brk, open);
4779 let exit = self.walk_stmt(body_stmt, body);
4780 self.loop_targets.pop();
4781 exit
4782 }
4783
4784 /// Walks a block, keeping [`Self::name_path`] current for the sequential
4785 /// block's own name.
4786 fn walk_block(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
4787 // `L: assert (..) ... disable L; ...`: one leaf (see the helper).
4788 if !self.entering_block && self_disabling_assertion_block(&stmt) {
4789 self.push(cur, stmt);
4790 return Ok(cur);
4791 }
4792 let name = (!self.entering_block
4793 && stmt.block_kind() == Some(StatementBlockKind::Sequential))
4794 .then(|| block_name(&stmt))
4795 .filter(|n| !n.is_empty());
4796 let pushed = name.is_some();
4797 if let Some(n) = name {
4798 self.name_path.push(n);
4799 }
4800 // LRM 19.3: `@@(begin blk)` / `@@(end blk)` of a named block, signalled
4801 // at its first statement and after its last (the codegen drops the
4802 // ones no covergroup names). Only a block that is walked into the CFG
4803 // gets here; a leaf block's events are emitted where it is lowered.
4804 let blk = (!self.entering_block
4805 && stmt.block_kind() == Some(StatementBlockKind::Sequential))
4806 .then(|| stmt.block_symbol())
4807 .flatten()
4808 .filter(|b| !b.name().is_empty());
4809 if let Some(b) = blk {
4810 self.block_event(cur, b, false);
4811 self.event_stack.push((b, self.disable_targets.len()));
4812 }
4813 let r = self.walk_block_inner(stmt, cur);
4814 if blk.is_some() {
4815 self.event_stack.pop();
4816 }
4817 if pushed {
4818 self.name_path.pop();
4819 }
4820 if let (Some(b), Ok(end)) = (blk, &r) {
4821 self.block_event(*end, b, true);
4822 }
4823 r
4824 }
4825
4826 fn walk_block_inner(
4827 &mut self,
4828 stmt: Statement<'d>,
4829 cur: BlockId,
4830 ) -> Result<BlockId, LoweringError> {
4831 // `StatementBlockKind` is a plain (non-`#[non_exhaustive]`) enum
4832 // hand-authored in `sv-lang`, so this literal `match` is genuinely,
4833 // compiler-checked exhaustive -- no wildcard needed or used.
4834 match stmt.block_kind() {
4835 // A named block some `disable` inside it can leave: its own exit.
4836 // A block declaring automatic variables: they are created and
4837 // initialized on each entry (LRM 1800-2023 §6.21), at the
4838 // start of a CFG block of its own.
4839 Some(_) if !self.entering_block && !block_automatic_vars(&stmt, true).is_empty() => {
4840 // A CFG block with no statements yet takes the bindings
4841 // itself (they run before its statements).
4842 let fresh = &self.blocks[cur.0 as usize];
4843 let entry = if fresh.stmts.is_empty() && fresh.step_exprs.is_empty() {
4844 cur
4845 } else {
4846 let entry = self.alloc_block();
4847 self.close(cur, Terminator::Fallthrough(entry));
4848 entry
4849 };
4850 for var in block_automatic_vars(&stmt, true) {
4851 self.blocks[entry.0 as usize]
4852 .bindings
4853 .push(ArgBinding::Init { var });
4854 }
4855 // A sequential block's handle-holding variables end with it.
4856 let owned: Vec<Symbol<'d>> =
4857 if stmt.block_kind() == Some(StatementBlockKind::Sequential) {
4858 block_automatic_vars(&stmt, true)
4859 .into_iter()
4860 .filter(|v| v.value_type().is_some_and(|t| may_hold_handles(&t)))
4861 .collect()
4862 } else {
4863 Vec::new()
4864 };
4865 let scoped = !owned.is_empty();
4866 if scoped {
4867 self.owned_scopes.push(owned.clone());
4868 }
4869 self.entering_block = true;
4870 let r = self.walk_block(stmt, entry);
4871 self.entering_block = false;
4872 if !scoped {
4873 return r;
4874 }
4875 self.owned_scopes.pop();
4876 let end = r?;
4877 let leave = self.alloc_block();
4878 self.close(end, Terminator::Fallthrough(leave));
4879 for var in owned.into_iter().rev() {
4880 self.blocks[leave.0 as usize]
4881 .bindings
4882 .push(ArgBinding::Release { var });
4883 }
4884 Ok(leave)
4885 }
4886 Some(StatementBlockKind::Sequential)
4887 if let name = block_name(&stmt)
4888 && !name.is_empty()
4889 && (is_disabled_somewhere(&name)
4890 || is_disabled_somewhere(&self.name_path.join("."))
4891 || contains_disable_through_calls(stmt.as_sem_node())) =>
4892 {
4893 self.entering_block = false;
4894 let exit = self.alloc_block();
4895 self.target_depth.insert(exit, self.owned_scopes.len());
4896 // LRM 9.6.2: another process's `disable` of this block resumes
4897 // the process at `exit`, wherever in the block it is suspended.
4898 let scoped = is_disabled_somewhere(&name);
4899 if scoped {
4900 self.scope_chain
4901 .push((name.clone(), exit, self.frame_stack.len()));
4902 }
4903 // `disable p.b.x;` of another process names the block by its
4904 // path from the process's outermost block: a second scope
4905 // under that dotted label tells same-named blocks apart.
4906 let dotted = self.name_path.join(".");
4907 let path_scoped = dotted.contains('.') && is_disabled_somewhere(&dotted);
4908 if path_scoped {
4909 self.scope_chain
4910 .push((dotted.clone(), exit, self.frame_stack.len()));
4911 }
4912 // `disable fl.b1.blk;` goes through the fork's own label `fl`
4913 // (9.6.2): the branch's path under that label tells the
4914 // same-named blocks of sibling branches apart.
4915 let fork_key = self
4916 .fork_name
4917 .as_ref()
4918 .map(|f| format!("{f}.{dotted}"))
4919 .filter(|k| is_disabled_somewhere(k));
4920 let fork_scoped = fork_key.is_some();
4921 if let Some(key) = fork_key {
4922 self.scope_chain.push((key, exit, self.frame_stack.len()));
4923 }
4924 self.disable_targets
4925 .push((name, exit, self.frame_stack.len()));
4926 let end = self.walk_seq(live_statements(&stmt), cur);
4927 self.disable_targets.pop();
4928 if fork_scoped {
4929 self.scope_chain.pop();
4930 }
4931 if path_scoped {
4932 self.scope_chain.pop();
4933 }
4934 if scoped {
4935 self.scope_chain.pop();
4936 }
4937 self.close(end?, Terminator::Fallthrough(exit));
4938 Ok(exit)
4939 }
4940 Some(StatementBlockKind::Sequential) => {
4941 self.entering_block = false;
4942 self.walk_seq(live_statements(&stmt), cur)
4943 }
4944 Some(
4945 kind @ (StatementBlockKind::JoinAll
4946 | StatementBlockKind::JoinAny
4947 | StatementBlockKind::JoinNone),
4948 ) => {
4949 // Task 1 (PR-069/`docs/design/process.md` §7): construction
4950 // only, never scheduled. Each branch is walked as its own,
4951 // fully independent `ProcessCfg` (`OneShot` -- a fork
4952 // branch runs to completion or suspends, but never
4953 // re-triggers the way an `always` process does).
4954 //
4955 // Verified live: `>1` un-`begin`/`end`-wrapped branches
4956 // (`fork a; b; join`) are NOT `stmt.statements()`'s direct
4957 // elements -- slang groups them into one `List` node first
4958 // (the same generic multi-statement wrapper `walk_stmt`'s
4959 // own `StatementKind::List` arm already flattens elsewhere),
4960 // so `statements()` here returns exactly one `List` element,
4961 // not one element per branch. A single, un-wrapped branch
4962 // (`fork a; join`) has no such wrapper. Flatten exactly one
4963 // `List` level when present, never more (a `List`
4964 // containing a nested `List` is not a shape any fixture
4965 // this task constructs, so silently over-flattening would
4966 // risk merging two real branches into one).
4967 let branch_stmts = live_statements(&stmt);
4968 let branch_stmts = match branch_stmts.as_slice() {
4969 [only] if only.kind() == StatementKind::List => live_statements(only),
4970 _ => branch_stmts,
4971 };
4972 self.entering_block = false;
4973 let fork_name = Some(block_name(&stmt)).filter(|n| !n.is_empty());
4974 // LRM 9.6.2: a `disable` of one branch's own named outermost
4975 // block, reachable from the parent (after this fork
4976 // statement), a sibling branch, or deeper still (threaded
4977 // into each branch's own `Inherited::known_fork_labels`
4978 // below) -- see `CfgBuilder::fork_branch_names`'s own doc
4979 // comment. Computed from a reference: `branch_stmts` is
4980 // still needed, un-consumed, by the `.into_iter()` build
4981 // below.
4982 // The fork block's own name (`fork : NAME`) tags every branch
4983 // process too (`ProcessCfg::disable_names`), so a later
4984 // `disable NAME;` of the parent, after the join, ends the
4985 // branches still running (LRM 9.6.2: all activities enabled
4986 // within the named block end).
4987 let new_branch_names: Vec<String> = branch_stmts
4988 .iter()
4989 .filter_map(|s| peer_block(s).map(|p| p.name))
4990 .collect();
4991 let known_fork_labels: Vec<String> = self
4992 .fork_branch_names
4993 .iter()
4994 .filter(|n| !self.fork_block_names.contains(n))
4995 .cloned()
4996 .chain(new_branch_names.iter().cloned())
4997 .collect();
4998 let ancestor_blocks: Vec<String> = self
4999 .ancestor_blocks
5000 .iter()
5001 .cloned()
5002 .chain(self.disable_targets.iter().map(|(n, _, _)| n.clone()))
5003 .collect();
5004 // Equal inherited context and statement give an equal CFG
5005 // (a fresh builder is seeded from `Inherited` alone), so a
5006 // task inlined at many call sites builds each of its forks'
5007 // branches once and the lowering compiles each once
5008 // ([`ProcessCfg::share_id`]). Not inside a recursive
5009 // activation (its capture temporaries are per site) or
5010 // under a clock inference.
5011 let memoizable = self.rec.is_none()
5012 && self.rec_up.is_empty()
5013 && self.global_clock.is_none()
5014 && self.checker_clock.is_none();
5015 let mut branches: Vec<ProcessCfg<'d>> = branch_stmts
5016 .into_iter()
5017 .map(|branch_stmt| {
5018 let key = memoizable.then(|| {
5019 MemoKey::Fork(ForkBranchKey {
5020 stmt: (stmt_addr(&branch_stmt), branch_stmt.kind() as u16),
5021 fork_name: fork_name.clone(),
5022 default_clocking: self.default_clocking.map(|c| c.uid()),
5023 this_var: self.this_stack.last().map(|t| t.uid()),
5024 ref_aliases: self
5025 .ref_stack
5026 .iter()
5027 .map(|(a, b)| (a.uid(), b.uid()))
5028 .collect(),
5029 rec_outer: self.rec_outer.clone(),
5030 labels: known_fork_labels.clone(),
5031 ancestors: ancestor_blocks.clone(),
5032 })
5033 });
5034 if let Some(k) = &key
5035 && let Some(hit) = self.fork_memo.borrow().get(k)
5036 {
5037 return Ok(hit.clone());
5038 }
5039 let mut cfg = build_cfg_in(
5040 &branch_stmt,
5041 ProcessLoopKind::OneShot,
5042 fork_name.clone(),
5043 self.default_clocking,
5044 None,
5045 self.checker_clock,
5046 Inherited {
5047 this_var: self.this_stack.last().copied(),
5048 ref_aliases: self.ref_stack.clone(),
5049 global_clock: self.global_clock,
5050 detached_trigger: false,
5051 forced_extern: None,
5052 rec_outer: self.rec_outer.clone(),
5053 fork_memo: Some(self.fork_memo.clone()),
5054 rec: self.rec.clone(),
5055 rec_up: self.rec_up.clone(),
5056 known_fork_labels: known_fork_labels.clone(),
5057 ancestor_blocks: ancestor_blocks.clone(),
5058 },
5059 self.virtual_tasks.clone(),
5060 )?;
5061 if let Some(k) = key {
5062 cfg.share_id = Some(
5063 NEXT_SHARE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
5064 );
5065 self.fork_memo.borrow_mut().insert(k, cfg.clone());
5066 }
5067 Ok(cfg)
5068 })
5069 .collect::<Result<_, LoweringError>>()?;
5070 for b in &mut branches {
5071 b.captured = captured_vars(b);
5072 // An aliased `ref` formal is the branch's storage too.
5073 b.captured
5074 .retain(|c| !self.ref_stack.iter().any(|(f, _)| f.uid() == c.uid()));
5075 // A branch inside an inlined class-method task shares the
5076 // forking activation's `this`.
5077 if let Some(t) = self.this_stack.last()
5078 && !b.captured.iter().any(|c| c.uid() == t.uid())
5079 {
5080 b.captured.push(*t);
5081 }
5082 }
5083 // The parent keeps these labels resolvable for the rest of
5084 // its own walk (not popped like `disable_targets`): LRM
5085 // 9.6.2 lets a branch be disabled for the rest of the
5086 // process's life, not only while this `fork` statement is
5087 // still being walked.
5088 self.fork_branch_names.extend(new_branch_names);
5089 if let Some(f) = &fork_name {
5090 self.fork_branch_names.push(f.clone());
5091 self.fork_block_names.push(f.clone());
5092 }
5093 let join_blk = self.alloc_block();
5094 self.close(
5095 cur,
5096 Terminator::ForkJoin {
5097 kind,
5098 branches,
5099 join_blk,
5100 },
5101 );
5102 Ok(join_blk)
5103 }
5104 None => Err(LoweringError::malformed(
5105 "Block statement with no block_kind (unexpected AST shape)",
5106 )),
5107 }
5108 }
5109
5110 fn walk_seq(
5111 &mut self,
5112 stmts: Vec<Statement<'d>>,
5113 mut cur: BlockId,
5114 ) -> Result<BlockId, LoweringError> {
5115 // LRM 14.11: a `##0` whose clocking event already occurred in this
5116 // time step "shall have no effect". Statically known exactly when the
5117 // process was just released by that event (`##N;` or
5118 // `@(cb);` on the default clocking, a `##0` included once it has waited) and only untimed, call-free
5119 // statements ran since; any other `##0` waits for the event (which is
5120 // right whenever the event has not yet fired this step).
5121 let mut released = false;
5122 for s in stmts {
5123 let s = unwrap_invalid(s);
5124 match self.default_clocking_wait(&s) {
5125 Some(0) if released => {
5126 if let Some(body) = s.body()
5127 && body.kind() != StatementKind::Empty
5128 {
5129 cur = self.walk_stmt(body, cur)?;
5130 }
5131 continue;
5132 }
5133 Some(_) => released = s.body().is_none_or(|b| b.kind() == StatementKind::Empty),
5134 None => {
5135 released = released && Self::untimed_simple(&s);
5136 }
5137 }
5138 cur = self.walk_stmt(s, cur)?;
5139 }
5140 Ok(cur)
5141 }
5142
5143 /// The cycle count of `##N` / `@(cb)` (count 1) on the default clocking
5144 /// when `stmt` is such a timed statement (see [`Self::walk_seq`]).
5145 fn default_clocking_wait(&self, stmt: &Statement<'d>) -> Option<u32> {
5146 let cb = self.default_clocking?;
5147 if stmt.kind() != StatementKind::Timed {
5148 return None;
5149 }
5150 let tc = stmt.timing_control()?;
5151 // A predicate, not a lowering: any other timing control is simply not
5152 // a wait on the default clocking (the next `##0` then waits).
5153 if tc.kind() == TimingControlKind::CycleDelay {
5154 first_expression_child(&tc)?
5155 .constant_value()?
5156 .as_i64()
5157 .and_then(|v| u32::try_from(v).ok())
5158 } else if tc.kind() == TimingControlKind::SignalEvent && tc.edge() == EdgeKind::None {
5159 let sym = first_expression_child(&tc)?.referenced_symbol()?;
5160 (sym.kind() == SymbolKind::ClockingBlock && sym.uid() == cb.uid()).then_some(1)
5161 } else {
5162 None
5163 }
5164 }
5165
5166 /// A statement that takes no time and calls no user subroutine: a system
5167 /// call or an untimed assignment.
5168 fn untimed_simple(stmt: &Statement<'d>) -> bool {
5169 stmt.kind() == StatementKind::ExpressionStatement
5170 && stmt.expr().is_some_and(|e| {
5171 (e.kind() == ExpressionKind::Call && e.is_system_call() == Some(true))
5172 || (e.kind() == ExpressionKind::Assignment && e.assignment_timing().is_none())
5173 })
5174 }
5175
5176 /// PR-044: `if`/`else` always expands to `Terminator::Branch`, regardless
5177 /// of whether either branch contains a suspend -- this is orthogonal to
5178 /// (never triggers) suspend splitting, which is exactly what
5179 /// `if_else_does_not_split_the_cfg` (`tests/process_cfg.rs`) proves: the
5180 /// number of *additional* blocks a wrapping suspend contributes is the
5181 /// same with or without the branch present.
5182 fn walk_conditional(
5183 &mut self,
5184 stmt: Statement<'d>,
5185 cur: BlockId,
5186 ) -> Result<BlockId, LoweringError> {
5187 if violation_check(&stmt).is_some() || is_clause_if(&stmt) {
5188 return self.walk_checked_if(stmt, cur);
5189 }
5190 let cond = stmt.expressions().into_iter().next().ok_or_else(|| {
5191 LoweringError::malformed("Conditional statement with no condition expression")
5192 })?;
5193 let then_stmt = stmt
5194 .then_branch()
5195 .ok_or_else(|| LoweringError::malformed("Conditional statement with no then-branch"))?;
5196 let else_stmt = stmt.else_branch();
5197
5198 // A constant predicate picks its arm at elaboration; only the live
5199 // arm becomes blocks (see `constant_if_arm`).
5200 if let Some(taken) = constant_if_arm(&stmt) {
5201 return match if taken { Some(then_stmt) } else { else_stmt } {
5202 Some(live) => self.walk_stmt(live, cur),
5203 None => Ok(cur),
5204 };
5205 }
5206
5207 // A call in the condition to a function that is inlined (a `fork` in
5208 // its body, an event output formal) runs ahead of the test; the
5209 // condition reads its result variable.
5210 let cur = self
5211 .hoist_suspending_calls_in(cond.as_sem_node(), cur)?
5212 .unwrap_or(cur);
5213 let then_blk = self.alloc_block();
5214 let else_blk = self.alloc_block();
5215 self.close(
5216 cur,
5217 Terminator::Branch {
5218 cond,
5219 then_blk,
5220 else_blk,
5221 },
5222 );
5223
5224 let then_exit = self.walk_stmt(then_stmt, then_blk)?;
5225 let else_exit = match else_stmt {
5226 Some(e) => self.walk_stmt(e, else_blk)?,
5227 None => else_blk,
5228 };
5229
5230 let join = self.alloc_block();
5231 self.close(then_exit, Terminator::Fallthrough(join));
5232 self.close(else_exit, Terminator::Fallthrough(join));
5233 Ok(join)
5234 }
5235
5236 /// A `unique`/`unique0`/`priority` `if` series (§12.4.2): one
5237 /// [`Terminator::Case`] over the whole series, `arms[i]` the then-statement
5238 /// of its i-th condition, `default_blk` the final `else` (empty without
5239 /// one), so the dispatch can report a violation (§12.4.2.1) where the
5240 /// series executes.
5241 fn walk_checked_if(
5242 &mut self,
5243 stmt: Statement<'d>,
5244 cur: BlockId,
5245 ) -> Result<BlockId, LoweringError> {
5246 let chain = if_series(stmt).map_err(|why| {
5247 LoweringError::unsupported(format!("{why} in an if series (malformed statement)"))
5248 })?;
5249 let arms: Vec<BlockId> = chain.thens.iter().map(|_| self.alloc_block()).collect();
5250 let default_blk = self.alloc_block();
5251 let join = self.alloc_block();
5252 self.close(
5253 cur,
5254 Terminator::Case {
5255 stmt,
5256 arms: arms.clone(),
5257 default_blk,
5258 },
5259 );
5260 for (then_stmt, &arm) in chain.thens.iter().zip(&arms) {
5261 let exit = self.walk_stmt(*then_stmt, arm)?;
5262 self.close(exit, Terminator::Fallthrough(join));
5263 }
5264 let default_exit = match chain.otherwise {
5265 Some(e) => self.walk_stmt(e, default_blk)?,
5266 None => default_blk,
5267 };
5268 self.close(default_exit, Terminator::Fallthrough(join));
5269 Ok(join)
5270 }
5271
5272 /// PR-006/PR-007: the one place a `BasicBlock` closes with
5273 /// `Terminator::Suspend` from a `Timed` statement. `Delay`/`SignalEvent`
5274 /// (posedge/negedge only) are the slice-scope half; Task 1 adds
5275 /// `EventList` (as `SuspendPoint::EdgeList`) and `CycleDelay`. **Task 6
5276 /// (CG-081/CG-171)** adds real handling for `ImplicitEvent` (as
5277 /// `SuspendPoint::ImplicitComb`) and `OneStepDelay` (as `SuspendPoint::
5278 /// OneStep`), and revises `CycleDelay` to carry its inferred clock
5279 /// signal/edge as real payload instead of discarding it. `Delay3` (not
5280 /// applicable to a procedural suspend point at all), `BlockEventList` (a
5281 /// verified, hard upstream `sv-lang`=0.1.9 API gap) and `RepeatedEvent`
5282 /// (verified structurally unreachable at this dispatch point -- see that
5283 /// arm below) remain named [`LoweringError`]s.
5284 /// See [`Self::walk_stmt`]'s doc comment: `TimingControlKind` is also
5285 /// `#[non_exhaustive]` upstream, so this dispatch is likewise an
5286 /// `if`/`else-if` chain (pinned by
5287 /// `determinism::kind_guard::timing_control_kind_count_matches_pinned_
5288 /// sv_lang_version`, `TimingControlKind::COUNT == 10`), not a literal
5289 /// `match`.
5290 fn walk_timed(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
5291 let tc = stmt
5292 .timing_control()
5293 .ok_or_else(|| LoweringError::malformed("Timed statement with no timing control"))?;
5294 if !class_event_wait_terms(&tc)?.is_empty() && class_event_wait_is_mixed(&tc) {
5295 // `@(ce or sig ...)` (LRM 1800-2023 9.4.2.1, 15.5): the process
5296 // waits on a class event (a runtime queue) and on signals (the
5297 // sensitivity table) at once. The leaf statement queues it on the
5298 // class events without blocking, the suspend is the signal
5299 // wait, and whichever wakes it first wins: the resume block leaves
5300 // the class queues (`ClassEventLeave`), and a class trigger
5301 // desensitises the signal side (`Engine::apply_sync_wake`).
5302 let guarded = stmt
5303 .body()
5304 .ok_or_else(|| LoweringError::malformed("Timed statement with no guarded body"))?;
5305 self.skip_class_event_terms = true;
5306 let point = self.suspend_point(tc, None);
5307 self.skip_class_event_terms = false;
5308 let point = point?;
5309 self.push(cur, stmt);
5310 let resume = self.alloc_block();
5311 self.blocks[resume.0 as usize]
5312 .bindings
5313 .push(ArgBinding::ClassEventLeave);
5314 self.close(
5315 cur,
5316 Terminator::Suspend {
5317 point,
5318 resume_blk: resume,
5319 },
5320 );
5321 return self.walk_stmt(guarded, resume);
5322 }
5323 if !class_event_wait_terms(&tc)?.is_empty() {
5324 // `@e` on a class event (LRM 1800-2023 15.5): the statement is the
5325 // leaf that queues the process on the event's runtime object; the
5326 // suspend after it lets the scheduler apply the block request,
5327 // like a blocking `semaphore.get`. Codegen lowers only the wait of
5328 // this leaf (the guarded body follows in the resumed block).
5329 let guarded = stmt
5330 .body()
5331 .ok_or_else(|| LoweringError::malformed("Timed statement with no guarded body"))?;
5332 if let Some(iff) = extract_iff(&tc) {
5333 // `@(e iff c)` (LRM 1800-2023 9.4.2.3): the event wakes the
5334 // process, which continues only if `c` holds then; otherwise
5335 // it waits for the event again. A loop around the plain wait.
5336 let header = self.alloc_block();
5337 let body = self.alloc_block();
5338 let exit = self.alloc_block();
5339 let resume = self.alloc_block();
5340 self.close(
5341 cur,
5342 Terminator::Loop {
5343 header,
5344 body,
5345 exit,
5346 entry: header,
5347 },
5348 );
5349 self.close(header, Terminator::Fallthrough(body));
5350 self.push(body, stmt);
5351 self.close(
5352 body,
5353 Terminator::Suspend {
5354 point: SuspendPoint::ByRequest,
5355 resume_blk: resume,
5356 },
5357 );
5358 self.close(
5359 resume,
5360 Terminator::Branch {
5361 cond: iff,
5362 then_blk: exit,
5363 else_blk: header,
5364 },
5365 );
5366 return self.walk_stmt(guarded, exit);
5367 }
5368 self.push(cur, stmt);
5369 let resume = self.alloc_block();
5370 self.close(
5371 cur,
5372 Terminator::Suspend {
5373 point: SuspendPoint::ByRequest,
5374 resume_blk: resume,
5375 },
5376 );
5377 return self.walk_stmt(guarded, resume);
5378 }
5379 self.allow_dynamic_cycle = true;
5380 let point = self.suspend_point(tc, stmt.body());
5381 self.allow_dynamic_cycle = false;
5382 let point = point?;
5383 let guarded = stmt
5384 .body()
5385 .ok_or_else(|| LoweringError::malformed("Timed statement with no guarded body"))?;
5386 if let Some(count) = self.dynamic_cycle_count.take() {
5387 // `##(expr)` with a run-time count (LRM 14.11): `repeat (expr)`
5388 // around one `##1` wait, the count evaluated once on entry. A
5389 // count <= 0 waits for no clock event.
5390 let entry = self.alloc_block();
5391 self.close(cur, Terminator::Fallthrough(entry));
5392 let counter = self.alloc_repeat_counter();
5393 self.set_repeat_init(entry, counter, RepeatCount::Expr(count));
5394 let header = self.alloc_block();
5395 let body = self.alloc_block();
5396 let exit = self.alloc_block();
5397 let resume = self.alloc_block();
5398 self.close(
5399 entry,
5400 Terminator::Loop {
5401 header,
5402 body,
5403 exit,
5404 entry: header,
5405 },
5406 );
5407 self.close(
5408 header,
5409 Terminator::RepeatCheck {
5410 counter,
5411 then_blk: body,
5412 else_blk: exit,
5413 },
5414 );
5415 self.close(
5416 body,
5417 Terminator::Suspend {
5418 point,
5419 resume_blk: resume,
5420 },
5421 );
5422 self.set_repeat_decrement(resume, counter);
5423 self.close(resume, Terminator::Fallthrough(header));
5424 return self.walk_stmt(guarded, exit);
5425 }
5426 let resume = self.alloc_block();
5427 self.close(
5428 cur,
5429 Terminator::Suspend {
5430 point,
5431 resume_blk: resume,
5432 },
5433 );
5434 self.walk_stmt(guarded, resume)
5435 }
5436
5437 /// `lhs = #d rhs;`, `lhs = @(e) rhs;`, `lhs = repeat (n) @(e) rhs;` (LRM
5438 /// 1800-2023 §10.4.1): the right-hand side is evaluated now into a
5439 /// compiler temporary ([`ArgBinding::Capture`]), the process waits, then
5440 /// the temporary is assigned ([`ArgBinding::AssignCaptured`]).
5441 fn walk_intra_assignment_timing(
5442 &mut self,
5443 assign: Expression<'d>,
5444 cur: BlockId,
5445 ) -> Result<BlockId, LoweringError> {
5446 let tc = assign
5447 .assignment_timing()
5448 .and_then(|n| n.as_timing_control())
5449 .ok_or_else(|| LoweringError::malformed("assignment timing is not a timing control"))?;
5450 let target = assign
5451 .left()
5452 .ok_or_else(|| LoweringError::malformed("assignment with no target"))?;
5453 let value = assign
5454 .right()
5455 .ok_or_else(|| LoweringError::malformed("assignment with no value"))?;
5456 let temp = self.captures.len() as u32;
5457 self.captures.push(target);
5458 // A fresh block: its bindings run before anything else in it, and
5459 // the capture must follow `cur`'s own statements.
5460 let capture = self.alloc_block();
5461 self.close(cur, Terminator::Fallthrough(capture));
5462 self.blocks[capture.0 as usize]
5463 .bindings
5464 .push(ArgBinding::Capture { temp, value });
5465 self.drive_clocking = drive_clocking_of(target);
5466 self.drive_via = drive_via_of(target);
5467 let after = if tc.kind() == TimingControlKind::RepeatedEvent {
5468 // `repeat (n) @(e)`: wait for `n` occurrences of `e` -- a counted
5469 // loop around one suspend (a count <= 0 waits for none).
5470 let count = first_expression_child(&tc).ok_or_else(|| {
5471 LoweringError::malformed("repeated event control with no count expression")
5472 })?;
5473 let event = tc
5474 .children()
5475 .into_iter()
5476 .find_map(|n| n.as_timing_control())
5477 .ok_or_else(|| {
5478 LoweringError::malformed("repeated event control with no event control")
5479 })?;
5480 let point = self.suspend_point(event, None)?;
5481 let counter = self.alloc_repeat_counter();
5482 self.set_repeat_init(capture, counter, RepeatCount::Expr(count));
5483 let header = self.alloc_block();
5484 let body = self.alloc_block();
5485 let exit = self.alloc_block();
5486 let resume = self.alloc_block();
5487 self.close(
5488 capture,
5489 Terminator::Loop {
5490 header,
5491 body,
5492 exit,
5493 entry: header,
5494 },
5495 );
5496 self.close(
5497 header,
5498 Terminator::RepeatCheck {
5499 counter,
5500 then_blk: body,
5501 else_blk: exit,
5502 },
5503 );
5504 self.close(
5505 body,
5506 Terminator::Suspend {
5507 point,
5508 resume_blk: resume,
5509 },
5510 );
5511 self.set_repeat_decrement(resume, counter);
5512 self.close(resume, Terminator::Fallthrough(header));
5513 exit
5514 } else {
5515 let point = self.suspend_point(tc, None)?;
5516 let resume = self.alloc_block();
5517 self.close(
5518 capture,
5519 Terminator::Suspend {
5520 point,
5521 resume_blk: resume,
5522 },
5523 );
5524 resume
5525 };
5526 self.drive_clocking = None;
5527 self.drive_via = None;
5528 self.blocks[after.0 as usize]
5529 .bindings
5530 .push(ArgBinding::AssignCaptured {
5531 temp,
5532 target,
5533 nba: false,
5534 });
5535 Ok(after)
5536 }
5537
5538 /// `lhs <= @(e) rhs;`, `lhs <= repeat (n) @(e) rhs;` (LRM 1800-2023
5539 /// §10.4.2, §9.4.5): the right-hand side (and the repeat count) are
5540 /// evaluated now into the compiler temporaries of this process, which
5541 /// goes straight on; a synthetic `join_none` branch, spawned here with
5542 /// copies of those temporaries ([`ProcessCfg::inherited`]), waits for
5543 /// the event control and then stages the nonblocking update.
5544 ///
5545 /// Deviation: the branch is a child of this process like any forked
5546 /// one, so a later `wait fork;` waits for the pending update and
5547 /// `disable fork;` cancels it.
5548 fn walk_nba_event(
5549 &mut self,
5550 stmt: Statement<'d>,
5551 assign: Expression<'d>,
5552 cur: BlockId,
5553 ) -> Result<BlockId, LoweringError> {
5554 let tc = assign
5555 .assignment_timing()
5556 .and_then(|n| n.as_timing_control())
5557 .ok_or_else(|| LoweringError::malformed("assignment timing is not a timing control"))?;
5558 let target = assign
5559 .left()
5560 .ok_or_else(|| LoweringError::malformed("assignment with no target"))?;
5561 let value = assign
5562 .right()
5563 .ok_or_else(|| LoweringError::malformed("assignment with no value"))?;
5564 let temp = self.captures.len() as u32;
5565 self.captures.push(target);
5566 let capture = self.alloc_block();
5567 self.close(cur, Terminator::Fallthrough(capture));
5568 self.blocks[capture.0 as usize]
5569 .bindings
5570 .push(ArgBinding::Capture { temp, value });
5571
5572 // The branch: its own builder, with this walk's context.
5573 let mut b = CfgBuilder::new();
5574 b.this_stack.extend(self.this_stack.last().copied());
5575 b.ref_stack = self.ref_stack.clone();
5576 b.global_clock = self.global_clock;
5577 b.virtual_tasks = self.virtual_tasks.clone();
5578 b.fork_branch_names = self.fork_branch_names.clone();
5579 b.ancestor_blocks = self.ancestor_blocks.clone();
5580 b.default_clocking = self.default_clocking;
5581 b.drive_clocking = drive_clocking_of(target);
5582 b.drive_via = drive_via_of(target);
5583 b.checker_clock = self.checker_clock;
5584 b.captures.push(target);
5585 let mut inherited = vec![InheritedSlot::Temp {
5586 parent: temp,
5587 child: 0,
5588 }];
5589 let entry = b.alloc_block();
5590 let event_roots = vec![target.as_sem_node(), tc.as_sem_node()];
5591 // `lhs <= ##(n) v` with a run-time count (LRM 14.11, 14.16): the count
5592 // is evaluated here and the branch loops one `##1` wait that often,
5593 // like `repeat (n) @(clk)`.
5594 let mut dyn_cycle: Option<(Expression<'d>, SuspendPoint)> = None;
5595 let mut const_point: Option<SuspendPoint> = None;
5596 if tc.kind() == TimingControlKind::CycleDelay {
5597 b.allow_dynamic_cycle = true;
5598 let point = b.suspend_point(tc, None);
5599 b.allow_dynamic_cycle = false;
5600 let point = point?;
5601 match b.dynamic_cycle_count.take() {
5602 Some(count) => dyn_cycle = Some((count, point)),
5603 None => const_point = Some(point),
5604 }
5605 }
5606 let after = if dyn_cycle.is_some() || tc.kind() == TimingControlKind::RepeatedEvent {
5607 let (count, point) = if let Some(d) = dyn_cycle.take() {
5608 d
5609 } else {
5610 let count = first_expression_child(&tc).ok_or_else(|| {
5611 LoweringError::malformed("repeated event control with no count expression")
5612 })?;
5613 let event = tc
5614 .children()
5615 .into_iter()
5616 .find_map(|n| n.as_timing_control())
5617 .ok_or_else(|| {
5618 LoweringError::malformed("repeated event control with no event control")
5619 })?;
5620 (count, b.suspend_point(event, None)?)
5621 };
5622 // The count is evaluated here, in this process.
5623 let parent_counter = self.alloc_repeat_counter();
5624 self.set_repeat_init(capture, parent_counter, RepeatCount::Expr(count));
5625 let counter = b.alloc_repeat_counter();
5626 inherited.push(InheritedSlot::Counter {
5627 parent: parent_counter,
5628 child: counter,
5629 });
5630 b.set_repeat_init(entry, counter, RepeatCount::Inherited(count));
5631 let header = b.alloc_block();
5632 let body = b.alloc_block();
5633 let exit = b.alloc_block();
5634 let resume = b.alloc_block();
5635 b.close(
5636 entry,
5637 Terminator::Loop {
5638 header,
5639 body,
5640 exit,
5641 entry: header,
5642 },
5643 );
5644 b.close(
5645 header,
5646 Terminator::RepeatCheck {
5647 counter,
5648 then_blk: body,
5649 else_blk: exit,
5650 },
5651 );
5652 b.close(
5653 body,
5654 Terminator::Suspend {
5655 point,
5656 resume_blk: resume,
5657 },
5658 );
5659 b.set_repeat_decrement(resume, counter);
5660 b.close(resume, Terminator::Fallthrough(header));
5661 exit
5662 } else {
5663 let point = match const_point.take() {
5664 Some(p) => p,
5665 None => b.suspend_point(tc, None)?,
5666 };
5667 let resume = b.alloc_block();
5668 b.close(
5669 entry,
5670 Terminator::Suspend {
5671 point,
5672 resume_blk: resume,
5673 },
5674 );
5675 resume
5676 };
5677 b.blocks[after.0 as usize]
5678 .bindings
5679 .push(ArgBinding::AssignCaptured {
5680 temp: 0,
5681 target,
5682 nba: true,
5683 });
5684 b.close(after, Terminator::End { loop_back: None });
5685 let inlined_locals = std::mem::take(&mut b.inlined_locals);
5686 let captures = std::mem::take(&mut b.captures);
5687 let ref_bindings = b.take_ref_bindings();
5688 let disable_scopes = b.take_disable_scopes();
5689 let disable_broadcast = std::mem::take(&mut b.disable_broadcast);
5690 let fork_branch_names = std::mem::take(&mut b.fork_branch_names);
5691 let mut branch = ProcessCfg {
5692 blocks: b.finish(),
5693 entry,
5694 body: stmt,
5695 inlined_locals,
5696 captures,
5697 captured: Vec::new(),
5698 ref_bindings,
5699 disable_names: Vec::new(),
5700 fork_branch_names,
5701 disable_scopes,
5702 disable_broadcast,
5703 inherited,
5704 capture_types: Vec::new(),
5705 recursion: Recursion::None,
5706 share_id: None,
5707 };
5708 // It shares the variables its event control and target name.
5709 branch.captured = captured_in(event_roots);
5710 branch
5711 .captured
5712 .retain(|c| !self.ref_stack.iter().any(|(f, _)| f.uid() == c.uid()));
5713 if let Some(t) = self.this_stack.last()
5714 && !branch.captured.iter().any(|c| c.uid() == t.uid())
5715 {
5716 branch.captured.push(*t);
5717 }
5718 let join_blk = self.alloc_block();
5719 self.close(
5720 capture,
5721 Terminator::ForkJoin {
5722 kind: StatementBlockKind::JoinNone,
5723 branches: vec![branch],
5724 join_blk,
5725 },
5726 );
5727 Ok(join_blk)
5728 }
5729
5730 /// A signal event's `(signal, edge)` with a checker's `$inferred_clock` (17.4)
5731 /// replaced by the clocking event of the instantiation context. Any other
5732 /// event is returned as written. A checker instantiated where no clock is
5733 /// inferred is a loud error, never a wait on nothing.
5734 fn resolve_inferred_clock(
5735 &self,
5736 signal: Expression<'d>,
5737 edge: EdgeKind,
5738 ) -> Result<InferredClockEvent<'d>, LoweringError> {
5739 if is_global_clock_call(&signal) {
5740 // `@$global_clock` (14.14, 17.7's counter_model): the global
5741 // clocking's event.
5742 return self.global_clock.ok_or_else(|| {
5743 LoweringError::unsupported(
5744 "`@$global_clock` as a procedural event control needs a `global clocking` \
5745 declared in the process's instance or an enclosing one (LRM 14.14); none \
5746 is in scope",
5747 )
5748 });
5749 }
5750 if !is_inferred_clock_call(&signal) {
5751 return Ok((signal, edge, None));
5752 }
5753 self.checker_clock.ok_or_else(|| {
5754 LoweringError::unsupported(
5755 "`$inferred_clock` (LRM 17.4) as the event control of a checker procedure: the \
5756 checker is instantiated where no default clocking applies (14.12), so no \
5757 clock is inferred",
5758 )
5759 })
5760 }
5761
5762 /// The suspend point a timing control waits on; `guarded` is the
5763 /// statement an `@*` implicit event control guards (its reads are the
5764 /// sensitivity).
5765 fn suspend_point(
5766 &mut self,
5767 tc: TimingControl<'d>,
5768 guarded: Option<Statement<'d>>,
5769 ) -> Result<SuspendPoint<'d>, LoweringError> {
5770 let tc_kind = tc.kind();
5771
5772 let point = if tc_kind == TimingControlKind::Invalid {
5773 return Err(LoweringError::malformed("Invalid timing control kind"));
5774 } else if tc_kind == TimingControlKind::Delay {
5775 let expr = first_expression_child(&tc).ok_or_else(|| {
5776 LoweringError::malformed("Delay timing control with no delay expression")
5777 })?;
5778 SuspendPoint::Delay(expr, self.frame_stack.last().map(|f| f.0))
5779 } else if tc_kind == TimingControlKind::SignalEvent {
5780 let edge = tc.edge();
5781 let signal = first_expression_child(&tc).ok_or_else(|| {
5782 LoweringError::malformed("SignalEvent timing control with no signal expression")
5783 })?;
5784 reject_chandle_event(&signal)?;
5785 let iff = extract_iff(&tc);
5786 let (signal, edge, inferred_iff) = self.resolve_inferred_clock(signal, edge)?;
5787 // `@clock iff c` on an inferred clock that itself has an `iff`
5788 // needs both guards (a conjunction, LRM 17.4): a one-member
5789 // event list carries them; `Edge` has room for one only.
5790 if let (Some(a), Some(b)) = (iff, inferred_iff) {
5791 if edge == EdgeKind::PosEdge || edge == EdgeKind::NegEdge {
5792 self.note_edge_signal(signal, edge);
5793 }
5794 return Ok(SuspendPoint::EdgeList(
5795 vec![(signal, edge)],
5796 vec![vec![a, b]],
5797 ));
5798 }
5799 let iff = iff.or(inferred_iff);
5800 if edge != EdgeKind::PosEdge && edge != EdgeKind::NegEdge {
5801 // `@(a)` (any change) / `@(edge a)`: a one-entry event list,
5802 // resolved through the program's wait table like `@(a or b)`.
5803 // With `iff` (LRM 1800-2023 9.4.2.3: `@(e iff c)` waits for a
5804 // change of `e`, or for the event, and then for `c`) it is a
5805 // single `Edge` term carrying its guard.
5806 if iff.is_some() {
5807 SuspendPoint::Edge { signal, edge, iff }
5808 } else {
5809 SuspendPoint::EdgeList(vec![(signal, edge)], vec![Vec::new()])
5810 }
5811 } else {
5812 self.note_edge_signal(signal, edge);
5813 SuspendPoint::Edge { signal, edge, iff }
5814 }
5815 } else if tc_kind == TimingControlKind::EventList {
5816 // Task 1: `@(a or b or c)` -- wake on any member. Each child is
5817 // itself a nested `TimingControl` (in practice always
5818 // `SignalEvent`, per the grammar). A member's `iff` (LRM
5819 // 1800-2023 9.4.2.3) rides along per member in the point's
5820 // second field.
5821 let mut entries = Vec::new();
5822 let mut iffs = Vec::new();
5823 for child in tc.children() {
5824 let sub_tc = child.as_timing_control().ok_or_else(|| {
5825 LoweringError::malformed(
5826 "EventList timing control with a non-timing-control child",
5827 )
5828 })?;
5829 let edge = sub_tc.edge();
5830 let signal = first_expression_child(&sub_tc).ok_or_else(|| {
5831 LoweringError::malformed(
5832 "EventList member timing control with no signal expression",
5833 )
5834 })?;
5835 if self.skip_class_event_terms && is_class_event_expr(&signal) {
5836 continue;
5837 }
5838 reject_chandle_event(&signal)?;
5839 let iff = extract_iff(&sub_tc);
5840 let (signal, edge, inferred_iff) = self.resolve_inferred_clock(signal, edge)?;
5841 if edge == EdgeKind::PosEdge || edge == EdgeKind::NegEdge {
5842 self.note_edge_signal(signal, edge);
5843 }
5844 entries.push((signal, edge));
5845 iffs.push(iff.into_iter().chain(inferred_iff).collect());
5846 }
5847 if entries.is_empty() {
5848 return Err(LoweringError::malformed(
5849 "EventList timing control with no member event controls",
5850 ));
5851 }
5852 SuspendPoint::EdgeList(entries, iffs)
5853 } else if tc_kind == TimingControlKind::CycleDelay {
5854 // Task 1 shape, Task 6 (CG-171) revised semantics -- see
5855 // `SuspendPoint::CycleDelay`'s own doc comment for the full
5856 // reasoning. Still a named `LoweringError`, never a silent
5857 // "assume 1 clock unit", when there is no unambiguous single
5858 // clock in scope.
5859 let count_expr = first_expression_child(&tc).ok_or_else(|| {
5860 LoweringError::malformed("CycleDelay timing control with no count expression")
5861 })?;
5862 let constant = count_expr
5863 .constant_value()
5864 .and_then(|v| v.as_i64())
5865 .and_then(|v| u32::try_from(v).ok());
5866 let count = match constant {
5867 Some(c) => c,
5868 None if self.allow_dynamic_cycle => {
5869 self.dynamic_cycle_count = Some(count_expr);
5870 1
5871 }
5872 None => {
5873 return Err(LoweringError::unsupported(
5874 "a non-constant cycle delay `##(expr)` in an intra-assignment timing \
5875 control (LRM 14.11 allows a run-time count only as a statement \
5876 delay; this simulator needs a compile-time constant here)",
5877 ));
5878 }
5879 };
5880 // The default clocking block's event (LRM 1800-2023 §14.11),
5881 // else the process's own inferred clock.
5882 let (clock, edge) = match (
5883 self.drive_clocking.or(self.default_clocking),
5884 self.inferred_clock,
5885 ) {
5886 (Some(cb), _) => (cb, EdgeKind::BothEdges),
5887 (None, Some(Some((signal, edge)))) => (
5888 signal.referenced_symbol().ok_or_else(|| {
5889 LoweringError::unsupported("CycleDelay (`##N`) clock with no symbol")
5890 })?,
5891 edge,
5892 ),
5893 _ => {
5894 return Err(LoweringError::unsupported(
5895 "CycleDelay (`##N`) with no unambiguous single inferred clock in scope \
5896 (no enclosing posedge/negedge edge, or more than one distinct clock \
5897 signal/edge seen) -- ARCHITECTURE §6.5's own \"ClockDomain the scheduler \
5898 needs\" requirement",
5899 ));
5900 }
5901 };
5902 let via = if self.drive_clocking.is_some() {
5903 self.drive_via
5904 } else {
5905 None
5906 };
5907 SuspendPoint::CycleDelay {
5908 count,
5909 clock,
5910 edge,
5911 via,
5912 }
5913 } else if tc_kind == TimingControlKind::ImplicitEvent {
5914 // Task 6 (CG-081): `@*`/`@(*)`. See `SuspendPoint::ImplicitComb`'s
5915 // own doc comment for why the read-set is captured here (AST-only,
5916 // no live Analysis handle reaches this crate).
5917 let body_stmt = guarded.ok_or_else(|| {
5918 LoweringError::unsupported(
5919 "an `@*` event control with no guarded statement (an intra-assignment `@*`)",
5920 )
5921 })?;
5922 SuspendPoint::ImplicitComb(implicit_comb_read_set(&body_stmt))
5923 } else if tc_kind == TimingControlKind::RepeatedEvent {
5924 // Task 6 (CG-081): `repeat(count) @(...)`. **Verified,
5925 // structurally unreachable at this dispatch point, never a scope
5926 // choice** -- empirically probed against the pinned
5927 // `sv-lang`=0.1.9 parser (a throwaway test compiling `initial q
5928 // <= repeat(3) @(posedge clk) 1;` and dumping the AST): a
5929 // `RepeatedEventControl` node is NEVER the outer `TimingControl`
5930 // of a `StatementKind::Timed` node at all. Two independent
5931 // parser facts combine to make this so: (1) `Parser::
5932 // parseStatement`'s own dispatch switch (`Parser_statements.cpp`)
5933 // sends a leading `repeat` keyword straight to
5934 // `parseLoopStatement` (producing `StatementKind::RepeatLoop`,
5935 // already handled by `walk_stmt`/`is_ordinary_statement_kind`) --
5936 // it never reaches `parseTimingControl()` at all, so
5937 // `repeat(3) @(posedge clk) q = 1;` used directly as a statement
5938 // parses as "run `@(posedge clk) q = 1;` 3 times", not "wait for
5939 // clk's 3rd posedge, then run `q = 1;` once". (2) The ONLY
5940 // grammar position `parseTimingControl()`'s own `RepeatKeyword`
5941 // case (`Parser_expressions.cpp`) is ever reached from is
5942 // `parseSubExpression`'s `isPossibleDelayOrEventControl` check --
5943 // i.e. `RepeatedEventControl` is only ever constructible as an
5944 // INTRA-ASSIGNMENT timing control embedded in an `Assignment`
5945 // expression's own `.children()` (confirmed empirically: `q <=
5946 // repeat(3) @(posedge clk) 1;` parses as one
5947 // `StatementKind::ExpressionStatement` whose `Assignment`
5948 // expression's `children()` is `[TimingControl(RepeatedEvent),
5949 // NamedValue, Conversion]` -- verified against
5950 // `AssignmentExpression::visitExprs` in the vendored slang C++,
5951 // `if (timingControl) timingControl->visit(visitor); left()...;
5952 // right()...;`), never a `Statement::timing_control()` at all.
5953 // The intra-assignment form is lowered by
5954 // `walk_intra_assignment_timing` (the right-hand side captured in
5955 // a `Temporary` frame slot before the counted wait), which asks
5956 // this function only for the inner event control's suspend
5957 // point -- so reaching this arm means a malformed AST.
5958 return Err(LoweringError::unsupported(
5959 "RepeatedEvent timing control (`repeat(n) @(...)`) can never be the outer \
5960 TimingControlKind of a Timed statement -- `repeat` at statement-position \
5961 always parses as StatementKind::RepeatLoop instead, and RepeatedEventControl \
5962 is only ever producible as an intra-assignment timing control, which \
5963 walk_intra_assignment_timing lowers",
5964 ));
5965 } else if tc_kind == TimingControlKind::OneStepDelay {
5966 // Task 6 (CG-081): `#0` / the built-in `1step` delay -- see
5967 // `SuspendPoint::OneStep`'s own doc comment.
5968 SuspendPoint::OneStep
5969 } else if tc_kind == TimingControlKind::BlockEventList {
5970 // LRM 19.3: a block event list `@@(begin f)` is only ever a
5971 // covergroup's sampling event (lowered by `covergroup` block-event
5972 // registration); slang rejects it as a procedural timing control
5973 // at parse ("expected statement"), so this arm is unreachable.
5974 return Err(LoweringError::unsupported(
5975 "a block event list `@@(...)` as a procedural timing control (LRM 19.3: \
5976 only a covergroup sampling event)",
5977 ));
5978 } else if tc_kind == TimingControlKind::Delay3 {
5979 // Delay3Control (`#(d1,d2,d3)`) is gate-primitive rise/fall/
5980 // turn-off delay syntax (LRM 28.14) -- not applicable to a
5981 // procedural `Timed` statement's suspend point at all (CG-081's
5982 // own 8-kind enumeration excludes it), so this stays a named
5983 // `LoweringError` unconditionally, not a gap this task closes.
5984 return Err(LoweringError::unsupported(
5985 "Delay3 timing control (gate-primitive rise/fall/turn-off delay syntax -- not \
5986 applicable to a procedural statement's suspend point)",
5987 ));
5988 } else {
5989 // Unreachable as long as `determinism::kind_guard`'s pinned
5990 // `TimingControlKind::COUNT == 10` still holds -- see
5991 // `walk_stmt`'s identical reasoning for `StatementKind`.
5992 return Err(LoweringError::malformed(format!(
5993 "unrecognized TimingControlKind {tc_kind:?} (sv-lang-kinds version drift -- \
5994 see determinism::kind_guard's TimingControlKind pin)"
5995 )));
5996 };
5997 Ok(point)
5998 }
5999
6000 /// Task 3 (PR-006's shape from Task 1, real semantics from CG-169/
6001 /// CG-115 here): `wait(expr) stmt;` (LRM 9.4.3). Unlike every other
6002 /// `Timed`/`Wait`-family suspend point, this one does NOT
6003 /// unconditionally close `cur` with `Terminator::Suspend` -- LRM 9.4.3 is
6004 /// an *immediate check with a conditional suspend*: if `expr` is already
6005 /// true the moment execution reaches it, there is NO suspend/resume
6006 /// round-trip at all (straight fallthrough into the guarded body, same
6007 /// call); only a false evaluation actually suspends. This is
6008 /// structurally the same "sometimes there's no suspend point at all"
6009 /// shape `docs/design/process.md` §2 calls out by name, and it is
6010 /// represented here exactly like any other runtime-decided branch: a
6011 /// `Terminator::Branch` whose `then_blk` is the guarded body's own entry
6012 /// block (`resume`) and whose `else_blk` is a fresh block that alone
6013 /// carries the real `Terminator::Suspend{point: SuspendPoint::Level(cond),
6014 /// resume_blk: resume}` -- `resume` is deliberately the SAME `BlockId` on
6015 /// both edges, so there genuinely is a suspend-free path from `cur` to
6016 /// the guarded body in the built `ProcessCfg` (a purely structural,
6017 /// compile-time fact -- codegen decides which edge is taken at
6018 /// simulation runtime by reusing the exact same generic branch-condition
6019 /// lowering `if`/`while` already use, `docs/design/process.md`'s own
6020 /// `is_definitely_true` reduction list explicitly includes "wait()
6021 /// condition" alongside "if"/"while" for this reason, PR-069's
6022 /// `Terminator`/`SuspendPoint` "cross-check against
6023 /// contains_suspend_trigger" extension policy). `resume`'s own entry is
6024 /// therefore executed either way (already-true fallthrough, or a genuine
6025 /// resume from the scheduler after the `Suspend` fires) -- the
6026 /// resume-prologue re-check that makes a spurious wake re-suspend
6027 /// without running the body is CG-115's own job, entirely on the
6028 /// codegen side (`sv-sim-codegen::process_lower::compile_iff_guard`);
6029 /// this crate never evaluates `expr` itself (no runtime state reaches
6030 /// this AST-only builder).
6031 fn walk_wait(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
6032 let cond = stmt.condition().ok_or_else(|| {
6033 LoweringError::malformed("Wait statement with no condition expression")
6034 })?;
6035 let resume = self.alloc_block();
6036 let suspend_blk = self.alloc_block();
6037 self.close(
6038 cur,
6039 Terminator::Branch {
6040 cond,
6041 then_blk: resume,
6042 else_blk: suspend_blk,
6043 },
6044 );
6045 self.close(
6046 suspend_blk,
6047 Terminator::Suspend {
6048 point: SuspendPoint::Level(cond),
6049 resume_blk: resume,
6050 },
6051 );
6052 let guarded = stmt
6053 .body()
6054 .ok_or_else(|| LoweringError::malformed("Wait statement with no guarded body"))?;
6055 self.walk_stmt(guarded, resume)
6056 }
6057
6058 /// `-> ev;` / `->> ev;` / `->> #d ev;` / `->> @(ctl) ev;` (LRM 1800-2023
6059 /// 15.5.1, 15.5.2). An `EventTrigger` with no timing, or with a `#d`
6060 /// delay, never suspends the issuer -- it is one ordinary leaf (codegen
6061 /// stages the delayed trigger as a nonblocking update). One with an event
6062 /// control (`->> @(ctl) ev;`) forks a detached one-shot process that
6063 /// waits on `ctl` and then triggers, so the issuer continues at once
6064 /// (`spawn_detached_trigger`).
6065 fn walk_event_trigger(
6066 &mut self,
6067 stmt: Statement<'d>,
6068 cur: BlockId,
6069 ) -> Result<BlockId, LoweringError> {
6070 let detached = std::mem::take(&mut self.detached_trigger);
6071 let Some(timing_node) = stmt.timing() else {
6072 self.push(cur, stmt);
6073 return Ok(cur);
6074 };
6075 let tc = timing_node.as_timing_control().ok_or_else(|| {
6076 LoweringError::malformed("EventTrigger timing is not a TimingControl node")
6077 })?;
6078 if tc.kind() == TimingControlKind::Delay {
6079 // `->> #d e;` never blocks the issuer: the trigger is staged as a
6080 // nonblocking update `d` ticks out (codegen reads the delay).
6081 self.push(cur, stmt);
6082 return Ok(cur);
6083 }
6084 if !detached {
6085 // `->> @(ctl) e;`: the issuer continues at once; a detached
6086 // process waits for `ctl`, then stages the trigger.
6087 return self.spawn_detached_trigger(stmt, cur, ProcessLoopKind::OneShot);
6088 }
6089 let point = self.suspend_point(tc, None)?;
6090 let resume = self.alloc_block();
6091 self.close(
6092 cur,
6093 Terminator::Suspend {
6094 point,
6095 resume_blk: resume,
6096 },
6097 );
6098 self.push(resume, stmt);
6099 Ok(resume)
6100 }
6101
6102 /// Forks the detached one-shot process of `->> @(ctl) e;` (see
6103 /// [`Self::detached_trigger`]) and continues the issuer past it.
6104 fn spawn_detached_trigger(
6105 &mut self,
6106 stmt: Statement<'d>,
6107 cur: BlockId,
6108 loop_kind: ProcessLoopKind,
6109 ) -> Result<BlockId, LoweringError> {
6110 let mut branch = build_cfg_in(
6111 &stmt,
6112 loop_kind,
6113 None,
6114 self.default_clocking,
6115 None,
6116 self.checker_clock,
6117 Inherited {
6118 this_var: self.this_stack.last().copied(),
6119 ref_aliases: self.ref_stack.clone(),
6120 global_clock: self.global_clock,
6121 detached_trigger: true,
6122 forced_extern: None,
6123 rec_outer: self.rec_outer.clone(),
6124 fork_memo: Some(self.fork_memo.clone()),
6125 rec: None,
6126 rec_up: Vec::new(),
6127 known_fork_labels: Vec::new(),
6128 ancestor_blocks: Vec::new(),
6129 },
6130 self.virtual_tasks.clone(),
6131 )?;
6132 branch.captured = captured_vars(&branch);
6133 branch
6134 .captured
6135 .retain(|c| !self.ref_stack.iter().any(|(f, _)| f.uid() == c.uid()));
6136 if let Some(t) = self.this_stack.last()
6137 && !branch.captured.iter().any(|c| c.uid() == t.uid())
6138 {
6139 branch.captured.push(*t);
6140 }
6141 let join_blk = self.alloc_block();
6142 self.close(
6143 cur,
6144 Terminator::ForkJoin {
6145 kind: StatementBlockKind::JoinNone,
6146 branches: vec![branch],
6147 join_blk,
6148 },
6149 );
6150 Ok(join_blk)
6151 }
6152
6153 /// `$async$and$array(mem, in, out);` and its seven siblings (LRM 1800-2023
6154 /// 20.16.1): the logic array is evaluated at the call, and again whenever
6155 /// an input term or any word of the personality memory changes. The
6156 /// issuer evaluates once (the call is an ordinary leaf) and forks a
6157 /// detached looping process that evaluates, waits on the operands' read
6158 /// set, and re-runs the same statement on each wake (the
6159 /// `detached_trigger` flag marks that process's own copy, so it does not
6160 /// fork again).
6161 fn walk_async_pla(
6162 &mut self,
6163 stmt: Statement<'d>,
6164 cur: BlockId,
6165 ) -> Result<BlockId, LoweringError> {
6166 let detached = std::mem::take(&mut self.detached_trigger);
6167 if !detached {
6168 self.push(cur, stmt);
6169 return self.spawn_detached_trigger(stmt, cur, ProcessLoopKind::Looping);
6170 }
6171 // Evaluate first, then wait: an input that changed between the fork
6172 // and this process's first run (same time step) is not missed.
6173 let reads = async_pla_read_set(&stmt);
6174 let resume = self.alloc_block();
6175 self.push(cur, stmt);
6176 self.close(
6177 cur,
6178 Terminator::Suspend {
6179 point: SuspendPoint::ImplicitComb(reads),
6180 resume_blk: resume,
6181 },
6182 );
6183 Ok(resume)
6184 }
6185
6186 /// A call statement `stmt` to the interface `extern` method `targets`
6187 /// (LRM 25.7), when the call needs the CFG: it runs the implementation
6188 /// inline (one), or as the branches of a `fork`...`join` (an `extern
6189 /// forkjoin` task with several, 25.7.5), or reports the run-time error of
6190 /// a forkjoin task nothing implements and returns with no effect.
6191 /// `None` when the call is an ordinary leaf (every implementation runs
6192 /// without suspending: the code generator calls them in turn).
6193 fn walk_extern_call(
6194 &mut self,
6195 stmt: Statement<'d>,
6196 call: Expression<'d>,
6197 targets: &crate::netlist::itf::ExternTargets<'d>,
6198 cur: BlockId,
6199 ) -> Result<Option<BlockId>, LoweringError> {
6200 // One branch of an enclosing fork: this implementation only.
6201 if let Some(imp) = self.forced_extern.take() {
6202 return self.inline_task_call(call, imp, cur).map(Some);
6203 }
6204 if targets.impls.is_empty() {
6205 let join = self.alloc_block();
6206 self.close(
6207 cur,
6208 Terminator::Switch {
6209 on: SwitchOn::NoTarget {
6210 what: format!(
6211 "no module connected to the interface implements extern forkjoin \
6212 task `{}`; the call has no effect",
6213 targets.proto.name()
6214 ),
6215 },
6216 arms: Vec::new(),
6217 default_blk: join,
6218 report: Some(call.source_range().start),
6219 },
6220 );
6221 return Ok(Some(join));
6222 }
6223 if !targets.impls.iter().any(subroutine_can_suspend) {
6224 return Ok(None);
6225 }
6226 if targets.impls.len() == 1 {
6227 return self.inline_task_call(call, targets.impls[0], cur).map(Some);
6228 }
6229 // Every implementation as a branch of one `fork`...`join` (the call
6230 // returns when all have).
6231 let mut branches = Vec::with_capacity(targets.impls.len());
6232 for imp in &targets.impls {
6233 let mut branch = build_cfg_in(
6234 &stmt,
6235 ProcessLoopKind::OneShot,
6236 None,
6237 self.default_clocking,
6238 None,
6239 self.checker_clock,
6240 Inherited {
6241 this_var: self.this_stack.last().copied(),
6242 ref_aliases: self.ref_stack.clone(),
6243 global_clock: self.global_clock,
6244 detached_trigger: false,
6245 forced_extern: Some(*imp),
6246 rec_outer: self.rec_outer.clone(),
6247 fork_memo: Some(self.fork_memo.clone()),
6248 rec: None,
6249 rec_up: Vec::new(),
6250 known_fork_labels: self.fork_branch_names.clone(),
6251 ancestor_blocks: self.ancestor_blocks.clone(),
6252 },
6253 self.virtual_tasks.clone(),
6254 )?;
6255 branch.captured = captured_vars(&branch);
6256 branch
6257 .captured
6258 .retain(|c| !self.ref_stack.iter().any(|(f, _)| f.uid() == c.uid()));
6259 if let Some(t) = self.this_stack.last()
6260 && !branch.captured.iter().any(|c| c.uid() == t.uid())
6261 {
6262 branch.captured.push(*t);
6263 }
6264 branches.push(branch);
6265 }
6266 let join_blk = self.alloc_block();
6267 self.close(
6268 cur,
6269 Terminator::ForkJoin {
6270 kind: StatementBlockKind::JoinAll,
6271 branches,
6272 join_blk,
6273 },
6274 );
6275 Ok(Some(join_blk))
6276 }
6277
6278 /// PR-045 (Task 1): `while (cond) body;`. `header` evaluates `cond`
6279 /// every iteration (re-checked, not cached) and branches to `body` or
6280 /// `exit`; `body`'s own exit closes the back edge to `header`; the real
6281 /// entry from `cur` is `header` (the condition is checked before the
6282 /// first iteration).
6283 fn walk_while_loop(
6284 &mut self,
6285 stmt: Statement<'d>,
6286 cur: BlockId,
6287 ) -> Result<BlockId, LoweringError> {
6288 let cond = stmt
6289 .condition()
6290 .ok_or_else(|| LoweringError::malformed("WhileLoop with no condition expression"))?;
6291 let body_stmt = stmt
6292 .body()
6293 .ok_or_else(|| LoweringError::malformed("WhileLoop with no body statement"))?;
6294
6295 let header = self.alloc_block();
6296 let body = self.alloc_block();
6297 let exit = self.alloc_block();
6298 // 38.36.1.2: before the condition, on every iteration.
6299 self.probe(header, stmt);
6300 self.close(
6301 cur,
6302 Terminator::Loop {
6303 header,
6304 body,
6305 exit,
6306 entry: header,
6307 },
6308 );
6309 self.close(
6310 header,
6311 Terminator::Branch {
6312 cond,
6313 then_blk: body,
6314 else_blk: exit,
6315 },
6316 );
6317 let body_exit = self.walk_loop_body(body_stmt, body, header, exit)?;
6318 self.close(body_exit, Terminator::Fallthrough(header));
6319 Ok(exit)
6320 }
6321
6322 /// PR-045 (Task 1): `do body; while (cond);`. Unlike `while`, `body`
6323 /// always runs once *before* any check -- the real entry from `cur` is
6324 /// `body` itself (`Terminator::Loop::entry`), and `header` (the
6325 /// post-body recheck, reached only via `body`'s own back edge) branches
6326 /// back to `body` or out to `exit`.
6327 fn walk_do_while_loop(
6328 &mut self,
6329 stmt: Statement<'d>,
6330 cur: BlockId,
6331 ) -> Result<BlockId, LoweringError> {
6332 let cond = stmt
6333 .condition()
6334 .ok_or_else(|| LoweringError::malformed("DoWhileLoop with no condition expression"))?;
6335 let body_stmt = stmt
6336 .body()
6337 .ok_or_else(|| LoweringError::malformed("DoWhileLoop with no body statement"))?;
6338
6339 let header = self.alloc_block();
6340 let body = self.alloc_block();
6341 let exit = self.alloc_block();
6342 self.close(
6343 cur,
6344 Terminator::Loop {
6345 header,
6346 body,
6347 exit,
6348 entry: body,
6349 },
6350 );
6351 self.close(
6352 header,
6353 Terminator::Branch {
6354 cond,
6355 then_blk: body,
6356 else_blk: exit,
6357 },
6358 );
6359 let body_exit = self.walk_loop_body(body_stmt, body, header, exit)?;
6360 self.close(body_exit, Terminator::Fallthrough(header));
6361 Ok(exit)
6362 }
6363
6364 /// PR-045 (Task 1): `forever body;`. No condition exists at all (LRM:
6365 /// unconditional) -- `header` unconditionally falls through to `body`,
6366 /// never to `exit`, so `exit` is allocated (every `Terminator::Loop`
6367 /// always names one, per PR-045's literal shape) but unreachable by any
6368 /// statically-analyzable path; only `disable`/`$finish` end a `forever`
6369 /// loop in practice, neither of which this function needs to wire.
6370 fn walk_forever_loop(
6371 &mut self,
6372 stmt: Statement<'d>,
6373 cur: BlockId,
6374 ) -> Result<BlockId, LoweringError> {
6375 let body_stmt = stmt
6376 .body()
6377 .ok_or_else(|| LoweringError::malformed("ForeverLoop with no body statement"))?;
6378
6379 let header = self.alloc_block();
6380 let body = self.alloc_block();
6381 let exit = self.alloc_block();
6382 // 38.36.1.2: when first encountered and on every iteration.
6383 self.probe(header, stmt);
6384 self.close(
6385 cur,
6386 Terminator::Loop {
6387 header,
6388 body,
6389 exit,
6390 entry: header,
6391 },
6392 );
6393 self.close(header, Terminator::Fallthrough(body));
6394 let body_exit = self.walk_loop_body(body_stmt, body, header, exit)?;
6395 self.close(body_exit, Terminator::Fallthrough(header));
6396 Ok(exit)
6397 }
6398
6399 /// PR-045 (Task 1): `for (init; cond; step) body;`. The declaration-form
6400 /// initializer (`for (int i = 0; ...)`) is a separate `VariableDeclaration`
6401 /// statement slang hoists *before* this `ForLoop` node in an implicit
6402 /// wrapper `Block` -- already handled for free by the ordinary `List`/
6403 /// `Block` walk, so this function never sees it. The pre-declared-var
6404 /// form (`for (i = 0; ...)`, `for_loop_initializers()` non-empty) is a
6405 /// named `LoweringError`: those initializers are bare `Expression`s, and
6406 /// (like `for_loop_steps()`, see `BasicBlock::step_exprs`'s doc comment)
6407 /// this crate has no way to run a bare pre-loop `Expression` as an
6408 /// ordinary leaf effect without fabricating a `Statement` node it does
6409 /// not have -- a real, if narrow, gap recorded in this task's
6410 /// deviations, not a silent drop. The condition (`stop expression`) is
6411 /// re-checked every iteration in `header`; the step expressions run via
6412 /// `step_exprs` on a dedicated `step` block between `body` and the back
6413 /// edge to `header`.
6414 fn walk_for_loop(
6415 &mut self,
6416 stmt: Statement<'d>,
6417 cur: BlockId,
6418 ) -> Result<BlockId, LoweringError> {
6419 // The classic Verilog form `for (i = 0; ...)` (pre-declared `i`):
6420 // its bare initializer expressions run as `step_exprs` on `cur`
6421 // itself -- after `cur`'s own statements, before the jump into
6422 // `header` -- the same bare-expression mechanism the loop's step
6423 // expressions use below.
6424 // 38.36.1.2: before any of the control expressions (the initializers
6425 // run as `cur`'s step expressions, after its statements) ...
6426 self.probe(cur, stmt);
6427 for init in stmt.for_loop_initializers() {
6428 self.push_step_expr(cur, init);
6429 }
6430 // `for (;;)`: an omitted stop expression is always true (LRM
6431 // 1800-2023 12.7.1), so `header` falls through to `body` unconditionally.
6432 let cond = stmt.condition();
6433 let body_stmt = stmt
6434 .body()
6435 .ok_or_else(|| LoweringError::malformed("ForLoop with no body statement"))?;
6436 let steps = stmt.for_loop_steps();
6437
6438 let header = self.alloc_block();
6439 let body = self.alloc_block();
6440 let exit = self.alloc_block();
6441 self.close(
6442 cur,
6443 Terminator::Loop {
6444 header,
6445 body,
6446 exit,
6447 entry: header,
6448 },
6449 );
6450 match cond {
6451 Some(cond) => self.close(
6452 header,
6453 Terminator::Branch {
6454 cond,
6455 then_blk: body,
6456 else_blk: exit,
6457 },
6458 ),
6459 None => self.close(header, Terminator::Fallthrough(body)),
6460 }
6461 let step = self.alloc_block();
6462 let body_exit = self.walk_loop_body(body_stmt, body, step, exit)?;
6463 self.close(body_exit, Terminator::Fallthrough(step));
6464 // ... and on every iteration before the incremental statement.
6465 self.probe(step, stmt);
6466 for s in steps {
6467 self.push_step_expr(step, s);
6468 }
6469 self.close(step, Terminator::Fallthrough(header));
6470 Ok(exit)
6471 }
6472
6473 /// PR-045: `foreach (arr[i, j, ...]) body;`. A dimension's bound is a
6474 /// static `ConstantRange` (`Statement::foreach_loop_dims`), never an
6475 /// `Expression`, so each named dimension is a counted loop on the
6476 /// `repeat` machinery (`RepeatCount::Const` elements, a `RepeatCheck`
6477 /// header), its index variable set by `ArgBinding::Const`/`Step`. The
6478 /// body is lowered once. A dynamically-sized dimension (`range() ==
6479 /// None`) is a named `LoweringError`.
6480 fn walk_foreach_loop(
6481 &mut self,
6482 stmt: Statement<'d>,
6483 cur: BlockId,
6484 ) -> Result<BlockId, LoweringError> {
6485 // Over a queue, dynamic or associative array: a leaf statement
6486 // codegen lowers as one loop (`stmt_shared::lower_collection_
6487 // foreach`) when its body cannot suspend.
6488 let over_collection = stmt
6489 .expressions()
6490 .first()
6491 .and_then(|a| a.expr_type())
6492 .is_some_and(|t| crate::state::storage::is_collection(&t));
6493 // A suspending body over a collection nested in a collection or in a
6494 // fixed-size array (`int a[2][]`, `int q[$][$]`): `walk_nested_foreach`.
6495 let nested_dyn = stmt
6496 .foreach_loop_dims()
6497 .iter()
6498 .skip(usize::from(over_collection))
6499 .any(|d| d.range().is_none());
6500 // A `return` in the body of an inlined task has to jump out of the
6501 // CFG's task body (LRM 1800-2023 13.4.1), which a leaf loop cannot do.
6502 let returns = escapes(stmt.as_sem_node(), false, !self.return_targets.is_empty());
6503 if nested_dyn && (contains_suspend_trigger(stmt.as_sem_node()) || returns) {
6504 return self.walk_nested_foreach(stmt, cur);
6505 }
6506 if over_collection {
6507 if contains_suspend_trigger(stmt.as_sem_node()) || returns {
6508 return self.walk_collection_foreach(stmt, cur);
6509 }
6510 self.push(cur, stmt);
6511 return Ok(cur);
6512 }
6513 // A fixed-size array of queues / dynamic arrays (`int a[3][]`): one
6514 // leaf loop when the body cannot suspend (`stmt_shared::
6515 // lower_fixed_outer_foreach`).
6516 if stmt.foreach_loop_dims().iter().any(|d| d.range().is_none())
6517 && !contains_suspend_trigger(stmt.as_sem_node())
6518 && !returns
6519 {
6520 self.push(cur, stmt);
6521 return Ok(cur);
6522 }
6523 let dims = stmt.foreach_loop_dims();
6524 let body_stmt = stmt
6525 .body()
6526 .ok_or_else(|| LoweringError::malformed("ForeachLoop with no body statement"))?;
6527 // One counted loop per named dimension, outermost first; a dimension
6528 // whose loop variable is omitted (`foreach (a[i, ])`) is not
6529 // iterated (LRM 1800-2023 §12.7.3).
6530 let mut loops = Vec::new();
6531 for d in &dims {
6532 let Some(var) = d.loop_var() else {
6533 continue;
6534 };
6535 let range = d.range().ok_or_else(|| {
6536 LoweringError::unsupported(
6537 "foreach over a dynamically-sized dimension (a dynamic array, queue or \
6538 associative array)",
6539 )
6540 })?;
6541 loops.push((var, i64::from(range.left), i64::from(range.right)));
6542 }
6543 // Every index omitted: no dimension is iterated, the body never runs.
6544 if loops.is_empty() {
6545 return Ok(cur);
6546 }
6547 self.walk_foreach_nest(&loops, body_stmt, cur, None)
6548 }
6549
6550 /// A suspending `foreach` where a queue / dynamic array is nested in a
6551 /// fixed-size array or in another collection (`int a[2][3][]`, `int
6552 /// d[][]`, `int q[2][$]`). One counted loop per named dimension,
6553 /// outermost first (LRM 1800-2023 12.7.3); a fixed dimension counts its
6554 /// static range, a collection dimension counts the size of the element
6555 /// reached through the enclosing indices ([`RepeatCount::NestedSize`]),
6556 /// read when that loop is entered. An omitted index in front of a
6557 /// collection dimension and an associative array in the chain are named
6558 /// errors (the leaf lowering refuses the same shapes).
6559 fn walk_nested_foreach(
6560 &mut self,
6561 stmt: Statement<'d>,
6562 cur: BlockId,
6563 ) -> Result<BlockId, LoweringError> {
6564 let arr = stmt.expressions()[0];
6565 let body_stmt = stmt
6566 .body()
6567 .ok_or_else(|| LoweringError::malformed("ForeachLoop with no body statement"))?;
6568 let dims = stmt.foreach_loop_dims();
6569 let last_dyn = dims.iter().rposition(|d| d.range().is_none());
6570 // Whether each dimension is an associative array (its variable is a
6571 // key): peel the array type one dimension at a time.
6572 let mut ty = arr.expr_type();
6573 let mut assoc_dims = Vec::new();
6574 for _ in &dims {
6575 assoc_dims.push(ty.as_ref().is_some_and(|t| t.is_associative_array()));
6576 ty = ty.and_then(|t| t.element_type());
6577 }
6578 let mut levels = Vec::new();
6579 for (k, d) in dims.iter().enumerate() {
6580 let Some(var) = d.loop_var() else {
6581 if last_dyn.is_some_and(|l| l > k) {
6582 return Err(LoweringError::unsupported(
6583 "foreach whose body waits with an omitted index in front of a nested \
6584 queue, dynamic-array or associative-array dimension",
6585 ));
6586 }
6587 continue;
6588 };
6589 let fixed = d.range().map(|r| (r.left, r.right));
6590 levels.push(NestStep {
6591 var,
6592 fixed,
6593 assoc: assoc_dims[k],
6594 });
6595 }
6596 self.walk_nest_levels(&levels, &mut Vec::new(), arr, body_stmt, cur, None)
6597 }
6598
6599 /// The loops of [`Self::walk_nested_foreach`], `levels[0]` outermost;
6600 /// `prefix` holds the enclosing levels. `break` leaves the whole nest.
6601 fn walk_nest_levels(
6602 &mut self,
6603 levels: &[NestStep<'d>],
6604 prefix: &mut Vec<NestStep<'d>>,
6605 arr: Expression<'d>,
6606 body_stmt: Statement<'d>,
6607 cur: BlockId,
6608 outer_exit: Option<BlockId>,
6609 ) -> Result<BlockId, LoweringError> {
6610 let Some(&step_info) = levels.first() else {
6611 return self.walk_stmt(body_stmt, cur);
6612 };
6613 let var = step_info.var;
6614 let entry = self.alloc_block();
6615 self.close(cur, Terminator::Fallthrough(entry));
6616 let counter = self.alloc_repeat_counter();
6617 let (count, first, by) = match step_info.fixed {
6618 Some((left, right)) => {
6619 let by: i64 = if left <= right { 1 } else { -1 };
6620 let n = (i64::from(left) - i64::from(right)).unsigned_abs() + 1;
6621 (RepeatCount::Const(n), i64::from(left) - by, by)
6622 }
6623 None if prefix.is_empty() => (RepeatCount::Size(arr), -1, 1),
6624 None => (
6625 RepeatCount::NestedSize {
6626 arr,
6627 path: prefix.clone(),
6628 },
6629 -1,
6630 1,
6631 ),
6632 };
6633 self.set_repeat_init(entry, counter, count);
6634 let header = self.alloc_block();
6635 let body = self.alloc_block();
6636 let exit = self.alloc_block();
6637 self.inlined_locals.push(var);
6638 if step_info.assoc {
6639 // Keys walk first/next, like a top-level associative array.
6640 self.blocks[entry.0 as usize]
6641 .bindings
6642 .push(ArgBinding::AssocKey {
6643 var,
6644 arr,
6645 path: prefix.clone(),
6646 next: false,
6647 });
6648 } else {
6649 self.blocks[entry.0 as usize]
6650 .bindings
6651 .push(ArgBinding::Const { var, value: first });
6652 self.blocks[header.0 as usize]
6653 .bindings
6654 .push(ArgBinding::Step { var, by });
6655 }
6656 self.close(
6657 entry,
6658 Terminator::Loop {
6659 header,
6660 body,
6661 exit,
6662 entry: header,
6663 },
6664 );
6665 self.close(
6666 header,
6667 Terminator::RepeatCheck {
6668 counter,
6669 then_blk: body,
6670 else_blk: exit,
6671 },
6672 );
6673 let latch = self.alloc_block();
6674 self.set_repeat_decrement(latch, counter);
6675 if step_info.assoc {
6676 self.blocks[latch.0 as usize]
6677 .bindings
6678 .push(ArgBinding::AssocKey {
6679 var,
6680 arr,
6681 path: prefix.clone(),
6682 next: true,
6683 });
6684 }
6685 self.close(latch, Terminator::Fallthrough(header));
6686 let outer = outer_exit.unwrap_or(exit);
6687 prefix.push(step_info);
6688 let body_exit = if levels.len() == 1 {
6689 self.walk_loop_body(body_stmt, body, latch, outer)?
6690 } else {
6691 self.walk_nest_levels(&levels[1..], prefix, arr, body_stmt, body, Some(outer))?
6692 };
6693 prefix.pop();
6694 self.close(body_exit, Terminator::Fallthrough(latch));
6695 Ok(exit)
6696 }
6697
6698 /// A `foreach` over a queue, dynamic or associative array whose body
6699 /// waits: a counted loop over the size the array has at loop entry (the
6700 /// `repeat` machinery, as for a fixed-size array). The index walks up
6701 /// from 0; an associative array's key starts at its first key and moves
6702 /// to the next one after each iteration (`first`/`next`, which is how
6703 /// LRM 1800-2023 §12.7.3 orders the keys).
6704 fn walk_collection_foreach(
6705 &mut self,
6706 stmt: Statement<'d>,
6707 cur: BlockId,
6708 ) -> Result<BlockId, LoweringError> {
6709 let arr = stmt.expressions()[0];
6710 let assoc = arr.expr_type().is_some_and(|t| t.is_associative_array());
6711 let dims = stmt.foreach_loop_dims();
6712 let body_stmt = stmt
6713 .body()
6714 .ok_or_else(|| LoweringError::malformed("ForeachLoop with no body statement"))?;
6715 // The inner dimensions are fixed-size ranges (`int d[][3]`): a
6716 // collection's elements are never themselves collections, so only
6717 // the outermost dimension is dynamic (LRM 1800-2023 §12.7.3).
6718 let mut rest = Vec::new();
6719 for d in dims.iter().skip(1) {
6720 let Some(v) = d.loop_var() else {
6721 continue;
6722 };
6723 let range = d.range().ok_or_else(|| {
6724 LoweringError::unsupported(
6725 "foreach over a dimension of a queue or dynamic array's element that is \
6726 itself a queue, dynamic or associative array",
6727 )
6728 })?;
6729 rest.push((v, i64::from(range.left), i64::from(range.right)));
6730 }
6731 let Some(var) = dims.first().and_then(|d| d.loop_var()) else {
6732 // Outermost index omitted: only the named inner dimensions run.
6733 return if rest.is_empty() {
6734 Ok(cur)
6735 } else {
6736 self.walk_foreach_nest(&rest, body_stmt, cur, None)
6737 };
6738 };
6739 let entry = self.alloc_block();
6740 self.close(cur, Terminator::Fallthrough(entry));
6741 let counter = self.alloc_repeat_counter();
6742 self.set_repeat_init(entry, counter, RepeatCount::Size(arr));
6743 let header = self.alloc_block();
6744 let body = self.alloc_block();
6745 let exit = self.alloc_block();
6746 self.inlined_locals.push(var);
6747 if assoc {
6748 self.blocks[entry.0 as usize]
6749 .bindings
6750 .push(ArgBinding::AssocKey {
6751 var,
6752 arr,
6753 path: Vec::new(),
6754 next: false,
6755 });
6756 } else {
6757 self.blocks[entry.0 as usize]
6758 .bindings
6759 .push(ArgBinding::Const { var, value: -1 });
6760 self.blocks[header.0 as usize]
6761 .bindings
6762 .push(ArgBinding::Step { var, by: 1 });
6763 }
6764 self.close(
6765 entry,
6766 Terminator::Loop {
6767 header,
6768 body,
6769 exit,
6770 entry: header,
6771 },
6772 );
6773 self.close(
6774 header,
6775 Terminator::RepeatCheck {
6776 counter,
6777 then_blk: body,
6778 else_blk: exit,
6779 },
6780 );
6781 let latch = self.alloc_block();
6782 self.set_repeat_decrement(latch, counter);
6783 if assoc {
6784 self.blocks[latch.0 as usize]
6785 .bindings
6786 .push(ArgBinding::AssocKey {
6787 var,
6788 arr,
6789 path: Vec::new(),
6790 next: true,
6791 });
6792 }
6793 self.close(latch, Terminator::Fallthrough(header));
6794 let body_exit = if rest.is_empty() {
6795 self.walk_loop_body(body_stmt, body, latch, exit)?
6796 } else {
6797 self.walk_foreach_nest(&rest, body_stmt, body, Some(exit))?
6798 };
6799 self.close(body_exit, Terminator::Fallthrough(latch));
6800 Ok(exit)
6801 }
6802
6803 /// The counted loops of [`Self::walk_foreach_loop`], `loops[0]`
6804 /// outermost: each is the `repeat` machinery counting the dimension's
6805 /// elements, its index variable walking from the left bound to the right
6806 /// bound (it starts one step before `left`; the header advances it
6807 /// before each iteration's count check).
6808 ///
6809 /// The nest is one loop to `break`/`continue` (LRM 1800-2023 §12.7.3,
6810 /// Verilator agrees): `continue` advances to the next element (the
6811 /// innermost dimension's latch) and `break` leaves the whole nest
6812 /// (`outer_exit`, the outermost dimension's exit).
6813 fn walk_foreach_nest(
6814 &mut self,
6815 loops: &[(Symbol<'d>, i64, i64)],
6816 body_stmt: Statement<'d>,
6817 cur: BlockId,
6818 outer_exit: Option<BlockId>,
6819 ) -> Result<BlockId, LoweringError> {
6820 let Some(&(var, left, right)) = loops.first() else {
6821 return self.walk_stmt(body_stmt, cur);
6822 };
6823 let step: i64 = if left <= right { 1 } else { -1 };
6824 let count = (left - right).unsigned_abs() + 1;
6825 // A fresh block for this loop's entry: `cur` may already carry
6826 // another counted loop's `repeat_init` (an enclosing foreach body).
6827 let entry = self.alloc_block();
6828 self.close(cur, Terminator::Fallthrough(entry));
6829 let counter = self.alloc_repeat_counter();
6830 self.set_repeat_init(entry, counter, RepeatCount::Const(count));
6831 let header = self.alloc_block();
6832 let body = self.alloc_block();
6833 let exit = self.alloc_block();
6834 self.inlined_locals.push(var);
6835 self.blocks[entry.0 as usize]
6836 .bindings
6837 .push(ArgBinding::Const {
6838 var,
6839 value: left - step,
6840 });
6841 self.blocks[header.0 as usize]
6842 .bindings
6843 .push(ArgBinding::Step { var, by: step });
6844 self.close(
6845 entry,
6846 Terminator::Loop {
6847 header,
6848 body,
6849 exit,
6850 entry: header,
6851 },
6852 );
6853 self.close(
6854 header,
6855 Terminator::RepeatCheck {
6856 counter,
6857 then_blk: body,
6858 else_blk: exit,
6859 },
6860 );
6861 // The latch counts the iteration down; `continue` comes here too.
6862 let latch = self.alloc_block();
6863 self.set_repeat_decrement(latch, counter);
6864 self.close(latch, Terminator::Fallthrough(header));
6865 let outer = outer_exit.unwrap_or(exit);
6866 let body_exit = if loops.len() == 1 {
6867 self.walk_loop_body(body_stmt, body, latch, outer)?
6868 } else {
6869 self.walk_foreach_nest(&loops[1..], body_stmt, body, Some(outer))?
6870 };
6871 self.close(body_exit, Terminator::Fallthrough(latch));
6872 Ok(exit)
6873 }
6874
6875 /// R-run Task 8 (PR-045/PR-006): `repeat(N) body;` when `body` contains
6876 /// a suspend point (the gate `walk_stmt`'s own dispatch already checked
6877 /// before calling this function). Mechanically similar to
6878 /// [`Self::walk_while_loop`] -- `header`'s own terminator carries the
6879 /// real per-iteration decision, `body`'s own terminator closes the back
6880 /// edge, `entry` is `header` (the counter is checked before the first
6881 /// iteration too, LRM: `repeat(0)` runs zero times) -- except the
6882 /// decision is [`Terminator::RepeatCheck`] against a compiler-
6883 /// synthesized [`RepeatCounterId`], not a re-evaluated user `Branch`
6884 /// condition, and `cur` (this loop's own entry block) additionally
6885 /// records a [`BasicBlock::repeat_init`] evaluating the count expression
6886 /// exactly once before ever reaching `header`. The loop body is walked
6887 /// recursively via the ordinary [`Self::walk_stmt`] entry point, so any
6888 /// suspend terminator nested inside it composes for free -- exactly the
6889 /// same reason `disable` "composes correctly across suspend boundaries
6890 /// for free" per `docs/design/process.md` §2, and the same recursive
6891 /// shape every other loop-walker in this file already uses.
6892 fn walk_repeat_loop(
6893 &mut self,
6894 stmt: Statement<'d>,
6895 cur: BlockId,
6896 ) -> Result<BlockId, LoweringError> {
6897 // R-run Task 4's `stmt_shared::lower_repeat_loop` already established
6898 // (and this file's own Task 4-era doc comments confirm) that
6899 // `RepeatLoop::condition()` is the count expression, not a boolean --
6900 // slang has no separate accessor for it, this AST node just reuses
6901 // the `condition()` grammar slot for `repeat`'s own count operand.
6902 let count_expr = stmt
6903 .condition()
6904 .ok_or_else(|| LoweringError::malformed("RepeatLoop with no count expression"))?;
6905 let body_stmt = stmt
6906 .body()
6907 .ok_or_else(|| LoweringError::malformed("RepeatLoop with no body statement"))?;
6908
6909 let counter = self.alloc_repeat_counter();
6910 self.set_repeat_init(cur, counter, RepeatCount::Expr(count_expr));
6911
6912 let header = self.alloc_block();
6913 let body = self.alloc_block();
6914 let exit = self.alloc_block();
6915 // 38.36.1.2: when first encountered and at every iteration (so one
6916 // more time than the loop body runs, the last one finding the count
6917 // exhausted -- the same shape the leaf lowering has).
6918 self.probe(header, stmt);
6919 self.close(
6920 cur,
6921 Terminator::Loop {
6922 header,
6923 body,
6924 exit,
6925 entry: header,
6926 },
6927 );
6928 self.close(
6929 header,
6930 Terminator::RepeatCheck {
6931 counter,
6932 then_blk: body,
6933 else_blk: exit,
6934 },
6935 );
6936 // The latch counts the iteration down; `continue` comes here too.
6937 let latch = self.alloc_block();
6938 self.set_repeat_decrement(latch, counter);
6939 self.close(latch, Terminator::Fallthrough(header));
6940 let body_exit = self.walk_loop_body(body_stmt, body, latch, exit)?;
6941 self.close(body_exit, Terminator::Fallthrough(latch));
6942 Ok(exit)
6943 }
6944}
6945
6946/// Whether `node` (a statement kept as one straight-line leaf) contains a
6947/// jump out of itself into the CFG around it: a `break`/`continue` not
6948/// inside a loop of its own (when `in_loop`, an enclosing CFG loop exists)
6949/// or a `return` (when `in_task`, the statement is part of an inlined task
6950/// body).
6951fn escapes(node: SemNode<'_>, in_loop: bool, in_task: bool) -> bool {
6952 let mut stack = vec![(node, false)];
6953 while let Some((n, nested_loop)) = stack.pop() {
6954 let mut inner = nested_loop;
6955 if let Some(s) = n.as_statement() {
6956 let kind = s.kind();
6957 if matches!(kind, StatementKind::Break | StatementKind::Continue)
6958 && in_loop
6959 && !nested_loop
6960 {
6961 return true;
6962 }
6963 if kind == StatementKind::Return && in_task {
6964 return true;
6965 }
6966 if matches!(
6967 kind,
6968 StatementKind::ForLoop
6969 | StatementKind::WhileLoop
6970 | StatementKind::DoWhileLoop
6971 | StatementKind::ForeverLoop
6972 | StatementKind::ForeachLoop
6973 | StatementKind::RepeatLoop
6974 ) {
6975 inner = true;
6976 }
6977 }
6978 stack.extend(n.children().into_iter().map(|c| (c, inner)));
6979 }
6980 false
6981}
6982
6983/// Whether `stmt` is a statement a VPI `cbStmt` callback fires for once, just
6984/// before it executes (IEEE 1800-2023 Table 38-6): the blocks, `if`, `case`,
6985/// `wait`, the procedural assignments / `force` / `release` / `disable` /
6986/// event triggers, task and system-task calls, and the delay and event
6987/// controls. This mirrors the statements the VPI model gives an object of
6988/// those types. The loops (`while`/`for`/`repeat`/`forever`) fire per
6989/// iteration instead and are not covered here.
6990pub fn is_entry_probe_kind(stmt: &Statement<'_>) -> bool {
6991 match stmt.kind() {
6992 StatementKind::Block
6993 | StatementKind::Conditional
6994 | StatementKind::Case
6995 | StatementKind::PatternCase
6996 | StatementKind::Wait
6997 | StatementKind::Disable
6998 | StatementKind::EventTrigger
6999 | StatementKind::ProceduralAssign
7000 | StatementKind::ProceduralDeassign => true,
7001 StatementKind::ExpressionStatement => stmt.expr().is_some_and(|e| {
7002 matches!(
7003 e.kind(),
7004 ExpressionKind::Assignment | ExpressionKind::Call | ExpressionKind::UnaryOp
7005 )
7006 }),
7007 StatementKind::Timed => {
7008 stmt.timing_control().map(|t| t.kind()) != Some(TimingControlKind::RepeatedEvent)
7009 }
7010 // Not in Table 38-6, or a loop (probed per iteration): no entry probe.
7011 StatementKind::Invalid
7012 | StatementKind::Empty
7013 | StatementKind::List
7014 | StatementKind::VariableDeclaration
7015 | StatementKind::Return
7016 | StatementKind::Continue
7017 | StatementKind::Break
7018 | StatementKind::DisableFork
7019 | StatementKind::ForLoop
7020 | StatementKind::RepeatLoop
7021 | StatementKind::ForeachLoop
7022 | StatementKind::WhileLoop
7023 | StatementKind::DoWhileLoop
7024 | StatementKind::ForeverLoop
7025 | StatementKind::ImmediateAssertion
7026 | StatementKind::ConcurrentAssertion
7027 | StatementKind::WaitFork
7028 | StatementKind::WaitOrder
7029 | StatementKind::RandCase
7030 | StatementKind::RandSequence
7031 | StatementKind::ProceduralChecker => false,
7032 other => unreachable!("statement kind {other:?} is newer than this classification"),
7033 }
7034}
7035
7036thread_local! {
7037 /// The text of each source file of the design (as it was parsed, so
7038 /// offsets of a [`sv_lang::SourceLoc`] index it), which `disable_label`
7039 /// reads a `disable` statement's target from, by file name. One file
7040 /// registered without a name ([`set_source_text`]) answers for every
7041 /// location.
7042 static DISABLE_SOURCE: std::cell::RefCell<Vec<(Option<String>, std::rc::Rc<str>)>> =
7043 const { std::cell::RefCell::new(Vec::new()) };
7044}
7045
7046/// Registers the text of the source file statements are elaborated from,
7047/// for `disable label;` (see [`disable_label`]); `None` clears it.
7048pub fn set_source_text(text: Option<&str>) {
7049 DISABLE_SOURCE.with(|s| {
7050 *s.borrow_mut() = text
7051 .map(|t| (None, std::rc::Rc::from(t)))
7052 .into_iter()
7053 .collect();
7054 });
7055 scan_disable_names(text.into_iter());
7056}
7057
7058thread_local! {
7059 /// What the registered source text names (see [`scan_disable_names`]):
7060 /// every `disable <name>;` target, and every task name.
7061 static DISABLE_NAMES: std::cell::RefCell<(
7062 std::collections::HashSet<String>,
7063 std::collections::HashSet<String>,
7064 )> = std::cell::RefCell::new(Default::default());
7065}
7066
7067/// Records which names a `disable <name>;` of the design targets, and which
7068/// names are tasks, from the registered source text (the binding cannot read a
7069/// `disable` target, see the module doc). Over-approximate on purpose (a word
7070/// in a comment counts): a name here only makes the builder keep a scope exit
7071/// for it ([`CfgBuilder::scope_chain`]) or accept it as a task of the design.
7072fn scan_disable_names<'a>(texts: impl Iterator<Item = &'a str>) {
7073 let mut labels = std::collections::HashSet::new();
7074 let mut tasks = std::collections::HashSet::new();
7075 let mut fn_labels = std::collections::HashSet::new();
7076 let ident = |c: u8| c == b'_' || c == b'$' || c.is_ascii_alphanumeric();
7077 for text in texts {
7078 let b = text.as_bytes();
7079 let word_at = |mut i: usize| -> Option<(&str, usize)> {
7080 while i < b.len() && b[i].is_ascii_whitespace() {
7081 i += 1;
7082 }
7083 let start = i;
7084 while i < b.len() && ident(b[i]) {
7085 i += 1;
7086 }
7087 (i > start).then(|| (&text[start..i], i))
7088 };
7089 let mut i = 0;
7090 // Inside a `function` body (a DPI prototype or `pure`/`extern`
7091 // declaration has none, and ends at its `;`).
7092 let (mut in_fn, mut proto) = (false, false);
7093 while i < b.len() {
7094 if !ident(b[i]) {
7095 if b[i] == b';' {
7096 proto = false;
7097 }
7098 i += 1;
7099 continue;
7100 }
7101 let start = i;
7102 while i < b.len() && ident(b[i]) {
7103 i += 1;
7104 }
7105 let boundary = start == 0 || (b[start - 1] != b'.' && b[start - 1] != b'`');
7106 match &text[start..i] {
7107 "import" | "export" | "extern" | "pure" if boundary => proto = true,
7108 "function" if boundary => in_fn = !proto,
7109 "endfunction" if boundary => in_fn = false,
7110 "disable" if boundary => {
7111 // `disable a.b.c;` (9.6.2) names its last segment.
7112 // A generate-block index (`g[0].bk`) is part of a segment.
7113 let skip_idx = |mut e: usize| -> usize {
7114 loop {
7115 let t = text[e..].trim_start();
7116 let lead = text[e..].len() - t.len();
7117 let Some(inner) = t.strip_prefix('[') else {
7118 return e;
7119 };
7120 match inner.find(']') {
7121 Some(c)
7122 if inner[..c].trim().bytes().all(|d| d.is_ascii_digit()) =>
7123 {
7124 e += lead + 1 + c + 1;
7125 }
7126 _ => return e,
7127 }
7128 }
7129 };
7130 if let Some((mut name, e0)) = word_at(i)
7131 && name != "fork"
7132 {
7133 let mut end = skip_idx(e0);
7134 while text[end..].trim_start().starts_with('.')
7135 && let Some((next, e)) = word_at(
7136 end + (text[end..].len() - text[end..].trim_start().len()) + 1,
7137 )
7138 {
7139 name = next;
7140 end = skip_idx(e);
7141 }
7142 if text[end..].trim_start().starts_with(';') {
7143 labels.insert(name.to_string());
7144 if in_fn {
7145 fn_labels.insert(name.to_string());
7146 }
7147 // The whole dotted path too ([`CfgBuilder::name_path`]).
7148 let full: String = text[i..end]
7149 .chars()
7150 .filter(|c| !c.is_ascii_whitespace())
7151 .collect();
7152 // Every dotted suffix too: `disable u1.main.inner;`
7153 // names the block `inner` of `main` by the path
7154 // below the instance (9.6.2).
7155 let mut rest = full.as_str();
7156 while let Some((_, tail)) = rest.split_once('.') {
7157 if tail.contains('.') {
7158 labels.insert(tail.to_string());
7159 }
7160 rest = tail;
7161 }
7162 if full.contains('.') {
7163 labels.insert(full);
7164 }
7165 }
7166 }
7167 }
7168 "task" if boundary => {
7169 if let Some((mut name, end)) = word_at(i) {
7170 if matches!(name, "automatic" | "static")
7171 && let Some((n, _)) = word_at(end)
7172 {
7173 name = n;
7174 }
7175 tasks.insert(name.to_string());
7176 }
7177 }
7178 _ => {}
7179 }
7180 }
7181 }
7182 DISABLE_NAMES.with(|n| *n.borrow_mut() = (labels, tasks));
7183 FN_DISABLE_LABELS.with(|n| *n.borrow_mut() = fn_labels);
7184}
7185
7186thread_local! {
7187 /// The targets of the `disable <name>;` statements written inside a
7188 /// `function` body (see [`scan_disable_names`]).
7189 static FN_DISABLE_LABELS: std::cell::RefCell<std::collections::HashSet<String>> =
7190 std::cell::RefCell::new(Default::default());
7191}
7192
7193/// Whether the design has a `disable <name>;` inside a function body: such a
7194/// function can end a block of its caller (LRM 9.6.2, 35.9), which the call
7195/// sites check for ([`SwitchOn::FnDisable`]). Designs without one pay nothing.
7196pub fn has_function_disables() -> bool {
7197 FN_DISABLE_LABELS.with(|n| !n.borrow().is_empty())
7198}
7199
7200/// Whether a function body of the design has `disable <label>;`.
7201pub fn is_function_disable_label(label: &str) -> bool {
7202 FN_DISABLE_LABELS.with(|n| n.borrow().contains(label))
7203}
7204
7205/// Whether some `disable` statement of the design names `label`.
7206fn is_disabled_somewhere(label: &str) -> bool {
7207 DISABLE_NAMES.with(|n| n.borrow().0.contains(label))
7208}
7209
7210/// Every dotted `disable a.b.c;` target (and dotted suffix of one) the design
7211/// writes, sorted.
7212pub fn dotted_disable_labels() -> Vec<String> {
7213 let mut v: Vec<String> = DISABLE_NAMES.with(|n| {
7214 n.borrow()
7215 .0
7216 .iter()
7217 .filter(|l| l.contains('.'))
7218 .cloned()
7219 .collect()
7220 });
7221 v.sort();
7222 v
7223}
7224
7225/// Whether `label` is the name of a task of the design.
7226pub fn is_task_name(label: &str) -> bool {
7227 DISABLE_NAMES.with(|n| n.borrow().1.contains(label))
7228}
7229
7230/// Registers the text of every source file of a multi-file design, each
7231/// under the file name its locations resolve to ([`set_source_session`]).
7232pub fn set_source_texts(files: Vec<(String, String)>) {
7233 scan_disable_names(files.iter().map(|(_, t)| t.as_str()));
7234 DISABLE_SOURCE.with(|s| {
7235 *s.borrow_mut() = files
7236 .into_iter()
7237 .map(|(name, text)| (Some(name), std::rc::Rc::from(text)))
7238 .collect();
7239 });
7240}
7241
7242/// Whether two file names denote the same file (equal, or the same file on
7243/// disk).
7244fn same_file(a: &str, b: &str) -> bool {
7245 a == b
7246 || std::fs::canonicalize(a)
7247 .ok()
7248 .is_some_and(|x| std::fs::canonicalize(b).ok() == Some(x))
7249}
7250
7251/// The registered text holding `at`: the unnamed one if that is what was
7252/// registered, else the named file `at` resolves to (through the session
7253/// registered by [`set_source_session`]).
7254fn source_text_at(at: sv_lang::SourceLoc) -> Option<std::rc::Rc<str>> {
7255 let files = DISABLE_SOURCE.with(|s| s.borrow().clone());
7256 if let [(None, text)] = files.as_slice() {
7257 return Some(text.clone());
7258 }
7259 let file = SOURCE_SESSION
7260 .with(|s| s.borrow().as_ref().and_then(|se| se.resolve_location(at)))?
7261 .file;
7262 files
7263 .into_iter()
7264 .find(|(n, _)| n.as_deref().is_some_and(|n| same_file(n, &file)))
7265 .map(|(_, text)| text)
7266}
7267
7268thread_local! {
7269 /// The main source file's name as given (`$info`'s `file:line`).
7270 static SOURCE_NAME: std::cell::RefCell<Option<String>> =
7271 const { std::cell::RefCell::new(None) };
7272}
7273
7274/// Registers the main source file's name (see [`source_location`]).
7275pub fn set_source_name(name: Option<&str>) {
7276 SOURCE_NAME.with(|s| *s.borrow_mut() = name.map(str::to_owned));
7277}
7278
7279/// The source text of `span`, from the file text registered by
7280/// [`set_source_text`] / [`set_source_texts`] (`None` if that file's text is
7281/// not registered, e.g. a span in an `` `include `` d file).
7282pub fn source_text_range(span: sv_lang::SourceSpan) -> Option<String> {
7283 source_text_at(span.start)?
7284 .get(span.start.offset as usize..span.end.offset as usize)
7285 .map(str::to_owned)
7286}
7287
7288thread_local! {
7289 /// The session the design was parsed with: resolves a source location
7290 /// to its file and line (included files and `` `line `` directives
7291 /// included).
7292 static SOURCE_SESSION: std::cell::RefCell<Option<sv_lang::Session>> =
7293 const { std::cell::RefCell::new(None) };
7294}
7295
7296/// Registers the session the design was parsed with (see
7297/// [`source_location`]); `None` clears it.
7298pub fn set_source_session(session: Option<sv_lang::Session>) {
7299 SOURCE_SESSION.with(|s| *s.borrow_mut() = session);
7300}
7301
7302/// `file:line` of `loc`, as slang's source manager reports it (honoring
7303/// `` `include `` and `` `line ``) through the session registered by
7304/// [`set_source_session`] -- the main source file under the name it was
7305/// given ([`set_source_name`]); without a session, counted in the main
7306/// source file registered by [`set_source_name`]/[`set_source_text`].
7307pub fn source_location(loc: sv_lang::SourceLoc) -> Option<String> {
7308 if let Some(at) = SOURCE_SESSION.with(|s| {
7309 s.borrow()
7310 .as_ref()
7311 .and_then(|session| session.resolve_location(loc))
7312 }) {
7313 let main = SOURCE_NAME.with(|s| s.borrow().clone());
7314 let file = match main {
7315 Some(m) if same_file(&at.file, &m) => m,
7316 _ => at.file,
7317 };
7318 return Some(format!("{file}:{}", at.line));
7319 }
7320 let name = SOURCE_NAME.with(|s| s.borrow().clone())?;
7321 let text = source_text_at(loc)?;
7322 let head = text.get(..(loc.offset as usize).min(text.len()))?;
7323 let line = head.bytes().filter(|&b| b == b'\n').count() + 1;
7324 Some(format!("{name}:{line}"))
7325}
7326
7327/// The segments of a hierarchical `disable a.b.c;` target (LRM 9.6.2), read
7328/// from the statement's source text like [`disable_label`]; `None` for a
7329/// simple or unreadable target.
7330pub fn disable_path(stmt: &Statement<'_>) -> Option<Vec<String>> {
7331 let text = source_text_range(stmt.source_range())?;
7332 let rest = text.trim().strip_prefix("disable")?;
7333 if !rest.starts_with(char::is_whitespace) {
7334 return None;
7335 }
7336 let label: String = rest
7337 .trim()
7338 .trim_end_matches(';')
7339 .chars()
7340 .filter(|c| !c.is_whitespace())
7341 .collect();
7342 let segs: Vec<String> = label.split('.').map(str::to_string).collect();
7343 // A generate-block segment carries its index (`g[0]`).
7344 let ident = |t: &str| {
7345 let t = match t.find('[') {
7346 Some(b)
7347 if t.ends_with(']')
7348 && t[b + 1..t.len() - 1].bytes().all(|d| d.is_ascii_digit())
7349 && b + 2 < t.len() =>
7350 {
7351 &t[..b]
7352 }
7353 Some(_) => return false,
7354 None => t,
7355 };
7356 !t.is_empty()
7357 && t.chars().enumerate().all(|(i, c)| {
7358 c == '_' || c.is_ascii_alphabetic() || (i > 0 && (c.is_ascii_digit() || c == '$'))
7359 })
7360 };
7361 (segs.len() > 1 && segs.iter().all(|t| ident(t))).then_some(segs)
7362}
7363
7364/// The name `disable` acts on in a statement the CFG builder kept: the plain
7365/// label, or the last segment of a hierarchical `a.b.c` the builder resolved to
7366/// a block of another process ([`disable_path`]).
7367pub fn disable_leaf_label(stmt: &Statement<'_>) -> Result<String, LoweringError> {
7368 match disable_path(stmt).and_then(|p| p.last().cloned()) {
7369 Some(leaf) => Ok(leaf),
7370 None => disable_label(stmt),
7371 }
7372}
7373
7374/// The target name of a `disable label;` statement. The pinned sv-lang
7375/// binding exposes no `DisableStatement::target` (see the module doc), so
7376/// it is read from the statement's own source span in the text registered
7377/// by [`set_source_text`] -- accepted only when that span reads exactly
7378/// `disable <identifier> ;`, so a span into another file (an `` `include ``)
7379/// is a named error, never a misread label.
7380pub fn disable_label(stmt: &Statement<'_>) -> Result<String, LoweringError> {
7381 let span = stmt.source_range();
7382 let text = source_text_range(span);
7383 let unreadable = || {
7384 LoweringError::unsupported(
7385 "disable statement whose target cannot be read (no registered source text, or \
7386 the statement is not in a registered source file)",
7387 )
7388 };
7389 let text = text.ok_or_else(unreadable)?;
7390 let rest = text.trim().strip_prefix("disable").ok_or_else(unreadable)?;
7391 let label = rest.trim().trim_end_matches(';').trim();
7392 let ident = !label.is_empty()
7393 && label.chars().enumerate().all(|(i, c)| {
7394 c == '_' || c.is_ascii_alphabetic() || (i > 0 && (c.is_ascii_digit() || c == '$'))
7395 });
7396 if !ident || !rest.starts_with(char::is_whitespace) {
7397 let hier = if label.contains('.') {
7398 "; disabling a task or interface method by hierarchical name would have to \
7399 end every activation of it in other processes (IEEE 1800-2023 9.6.2)"
7400 } else {
7401 ""
7402 };
7403 return Err(LoweringError::unsupported(format!(
7404 "disable of `{label}` (only a simple block name is supported{hier})"
7405 )));
7406 }
7407 Ok(label.to_string())
7408}
7409
7410/// Whether `node`'s subtree contains a `disable label;` statement, in itself
7411/// or in the body of a task it calls (an inlined task's `disable` may target
7412/// the caller's block, LRM 9.6.2).
7413fn contains_disable_through_calls(node: SemNode<'_>) -> bool {
7414 let mut visiting = std::collections::HashSet::new();
7415 let mut stack = vec![node];
7416 while let Some(n) = stack.pop() {
7417 if n.as_statement()
7418 .is_some_and(|s| s.kind() == StatementKind::Disable)
7419 {
7420 return true;
7421 }
7422 if let Some(e) = n.as_expression()
7423 && let Some(callee) = user_task_callee(&e)
7424 && let Some(body) = proc_body(&callee)
7425 && visiting.insert(callee.uid())
7426 {
7427 stack.push(body.as_sem_node());
7428 }
7429 stack.extend(live_children(&n));
7430 }
7431 false
7432}
7433
7434/// Whether `node`'s subtree holds a `fork`/`join*` block, in itself or in the
7435/// body of a task it calls.
7436fn contains_fork_through_calls(node: SemNode<'_>) -> bool {
7437 let mut visiting = std::collections::HashSet::new();
7438 let mut stack = vec![node];
7439 while let Some(n) = stack.pop() {
7440 if n.as_statement().is_some_and(|s| {
7441 matches!(
7442 s.block_kind(),
7443 Some(
7444 StatementBlockKind::JoinAll
7445 | StatementBlockKind::JoinAny
7446 | StatementBlockKind::JoinNone
7447 )
7448 )
7449 }) {
7450 return true;
7451 }
7452 if let Some(e) = n.as_expression()
7453 && let Some(callee) = user_task_callee(&e)
7454 && let Some(body) = proc_body(&callee)
7455 && visiting.insert(callee.uid())
7456 {
7457 stack.push(body.as_sem_node());
7458 }
7459 stack.extend(live_children(&n));
7460 }
7461 false
7462}
7463
7464/// Every `StatementKind` variant [`CfgBuilder::walk_stmt`] treats as ordinary
7465/// straight-line code (PR-006's own acceptance text) -- i.e. every variant
7466/// that is not one of the 5 explicitly-named statement kinds
7467/// (`List`/`Block`/`Conditional`/`Timed`/the 5 suspend-rejecting kinds) it
7468/// handles directly. Kept as a `matches!` (not a literal `match`, see
7469/// `walk_stmt`'s doc comment) enumerating all 22 remaining variants of
7470/// today's pinned `StatementKind::COUNT == 32`.
7471/// Task 1 removed `ForLoop`/`WhileLoop`/`DoWhileLoop`/`ForeverLoop`/
7472/// `ForeachLoop`/`Disable` from this set. The five loop kinds now dispatch
7473/// to dedicated handlers (PR-045's `Terminator::Loop`) instead of being
7474/// treated as opaque leaves; `Disable` also gets a dedicated `walk_stmt` arm
7475/// (a jump to its named block's exit -- see this file's module doc comment's
7476/// `disable label;` section), never the "push as ordinary code" treatment
7477/// this function's callers give everything below. `RepeatLoop` (a plain statement-level `repeat(N)
7478/// stmt;`, distinct from the `RepeatedEvent` *timing control*) stays
7479/// ordinary -- PR-045's own requirement text names exactly `for`/`while`/
7480/// `do-while`/`forever`/`foreach`, not `repeat`.
7481fn is_ordinary_statement_kind(kind: StatementKind) -> bool {
7482 matches!(
7483 kind,
7484 StatementKind::Empty
7485 | StatementKind::ExpressionStatement
7486 | StatementKind::VariableDeclaration
7487 | StatementKind::Return
7488 | StatementKind::Continue
7489 | StatementKind::Break
7490 | StatementKind::Case
7491 | StatementKind::PatternCase
7492 | StatementKind::RepeatLoop
7493 | StatementKind::ImmediateAssertion
7494 | StatementKind::ConcurrentAssertion
7495 | StatementKind::ProceduralAssign
7496 | StatementKind::ProceduralDeassign
7497 | StatementKind::RandCase
7498 | StatementKind::RandSequence
7499 | StatementKind::ProceduralChecker
7500 )
7501}
7502
7503/// R-run Task 1: `true` iff `stmt` (already known to be a `StatementKind::
7504/// ExpressionStatement`) is a bare `$finish`/`$finish(n)`/`$stop`/`$fatal`
7505/// system-call TASK statement -- the exact same `ExpressionKind::Call` +
7506/// `is_system_call() == Some(true)` + `system_subroutine().known_name_id()`
7507/// recognition `sv-sim-codegen::process_lower::lower_system_call_statement`
7508/// already uses for its own dispatch (mirrored, not re-derived differently:
7509/// both call sites must agree on what counts as one of these three calls).
7510/// `$fatal`'s own optional finish_number/message arguments and `$stop`'s own
7511/// interactive-debugger-break runtime semantics are unaffected by this CFG-
7512/// level check -- it only ever answers "does the calling process's own
7513/// control flow end here", never anything about the call's own argument
7514/// list or its eventual codegen lowering (`$stop`/`$fatal` still lower to a
7515/// named `LoweringError` in `process_lower.rs` today, exactly as before this
7516/// task; this predicate firing for them one file earlier changes nothing
7517/// observable about that until a later task actually lowers them).
7518fn is_finish_or_stop_call(stmt: &Statement<'_>) -> bool {
7519 let Some(e) = stmt.expr() else {
7520 return false;
7521 };
7522 if e.kind() != ExpressionKind::Call || e.is_system_call() != Some(true) {
7523 return false;
7524 }
7525 let Some(sub) = e.system_subroutine() else {
7526 return false;
7527 };
7528 matches!(
7529 sub.known_name_id(),
7530 KnownSystemName::Finish | KnownSystemName::Stop | KnownSystemName::Fatal
7531 )
7532}
7533
7534/// The `unique`/`unique0`/`priority` qualifier on an `if` or `case` statement
7535/// (LRM 1800-2023 §12.4.2, §12.5.3), `None` for an unqualified statement or
7536/// any other kind. A `case ... matches` ([`StatementKind::PatternCase`])
7537/// carries its qualifier under its own accessor.
7538pub fn violation_check(stmt: &Statement<'_>) -> Option<UniquePriorityCheck> {
7539 let check = if stmt.kind() == StatementKind::PatternCase {
7540 stmt.pattern_case_check()
7541 } else {
7542 stmt.check()
7543 };
7544 match check {
7545 Some(UniquePriorityCheck::None) | None => None,
7546 Some(check) => Some(check),
7547 }
7548}
7549
7550/// The `if` / `else if` series a `unique`/`unique0`/`priority` qualifier on
7551/// the first `if` applies to (§12.4.2: "the entire series of if-else-if
7552/// conditions"): the condition and then-statement of each link, in source
7553/// order, and the final `else` statement if there is one. A nested
7554/// `else if` carries no qualifier of its own (the grammar has none); an
7555/// `else` holding a qualified `if` of its own is the final `else` and a
7556/// separate statement.
7557pub struct IfChain<'d> {
7558 /// The `if` statement of each link (its predicate is `links[i]
7559 /// .conditions()`: plain, `&&&`-chained, or pattern-matching, §12.6).
7560 pub links: Vec<Statement<'d>>,
7561 pub thens: Vec<Statement<'d>>,
7562 pub otherwise: Option<Statement<'d>>,
7563}
7564
7565/// Peels `Invalid` wrappers off `stmt` (IEEE 1800-2023 12.4/12.5).
7566///
7567/// slang suppresses the error of a construct inside a statically dead `if`
7568/// or `case` arm (e.g. a reversed range select `v[7:8]` of a descending
7569/// vector under a false parameter) but demotes every enclosing statement to
7570/// `Invalid`. Such wrappers nest (an `Invalid` body wraps a `Block` that wraps
7571/// an `Invalid` wrapping the `Conditional`), so this unwraps recursively via
7572/// `Statement::invalid_child`. An `Invalid` with no recoverable child is
7573/// returned unchanged, so the caller's loud "Invalid statement" refusal still
7574/// fires for it.
7575pub fn unwrap_invalid<'d>(mut stmt: Statement<'d>) -> Statement<'d> {
7576 while stmt.kind() == StatementKind::Invalid {
7577 match stmt.invalid_child() {
7578 Some(child) => stmt = child,
7579 None => break,
7580 }
7581 }
7582 stmt
7583}
7584
7585/// The child statements of a `List`/`Block`, with each statement slang
7586/// demoted to `Invalid` (a statically dead `if`/`case` arm it could not
7587/// bind) unwrapped to the real one. `Statement::statements` skips `Invalid`
7588/// children, which would silently drop the live statement (LRM 12.4/12.5).
7589pub fn live_statements<'d>(stmt: &Statement<'d>) -> Vec<Statement<'d>> {
7590 stmt.as_sem_node()
7591 .children_with_invalid()
7592 .into_iter()
7593 .filter_map(|n| n.as_statement())
7594 .map(unwrap_invalid)
7595 .collect()
7596}
7597
7598/// The children of `node` for a whole-subtree walk, with each `Invalid`
7599/// statement or expression (slang's demotion of a construct inside a
7600/// statically dead `if`/`case` arm it could not bind, LRM 12.4/12.5) replaced
7601/// by the real node it wraps. `SemNode::children` skips `Invalid` children,
7602/// so a call or read inside the live arm of such a statement would otherwise
7603/// be invisible to every discovery walk (called-subroutine discovery, write
7604/// sets, unsupported-construct scans).
7605pub fn live_children<'d>(node: &SemNode<'d>) -> Vec<SemNode<'d>> {
7606 node.children_with_invalid()
7607 .into_iter()
7608 .map(|n| {
7609 if let Some(s) = n.as_statement() {
7610 return unwrap_invalid(s).as_sem_node();
7611 }
7612 if let Some(mut e) = n.as_expression() {
7613 while e.kind() == ExpressionKind::Invalid
7614 && let Some(c) = e.invalid_child()
7615 {
7616 e = c;
7617 }
7618 return e.as_sem_node();
7619 }
7620 n
7621 })
7622 .collect()
7623}
7624
7625/// The body statement of a procedural block with any `Invalid` wrappers from
7626/// a statically dead arm peeled off (see [`unwrap_invalid`]). Every pass that
7627/// inspects a process's head timing control, read set or statements must go
7628/// through this rather than `Symbol::body`, so they all see the same live
7629/// body (otherwise an `always @*` loses its sensitivity).
7630pub fn proc_body<'d>(block: &Symbol<'d>) -> Option<Statement<'d>> {
7631 block.body().map(unwrap_invalid)
7632}
7633
7634/// The `StatementBlock` scopes whose locals `stmt` (a `Block`) declares.
7635///
7636/// Normally that is `Statement::block_symbol()`. slang never attaches the
7637/// symbol to a block that failed to bind (`Statement::bindBlock` returns the
7638/// `Invalid` wrapper before setting `blockSymbol`), so a `begin`/`end` that
7639/// holds a statically dead `if`/`case` arm it could not bind (LRM 12.4/12.5)
7640/// is recovered through `invalid_child` with no symbol, and its locals would
7641/// be invisible. For such a block the scopes are recovered from the variables
7642/// the live subtree references: the `StatementBlock` parents of its
7643/// `NamedValue` symbols. That can include an enclosing or nested block's
7644/// scope, which is harmless for the union-style local collections that call
7645/// this; a local no live statement references is not recovered (it has no
7646/// observable use).
7647pub fn block_scopes<'d>(stmt: &Statement<'d>) -> Vec<Symbol<'d>> {
7648 if let Some(bs) = stmt.block_symbol() {
7649 return vec![bs];
7650 }
7651 let node = stmt.as_sem_node();
7652 if stmt.kind() != StatementKind::Block || !contains_invalid(node) {
7653 return Vec::new();
7654 }
7655 let mut out: Vec<Symbol<'d>> = Vec::new();
7656 let mut seen = HashSet::new();
7657 let mut stack = vec![node];
7658 while let Some(n) = stack.pop() {
7659 if let Some(e) = n.as_expression()
7660 && crate::symid::is_value_name(e.kind())
7661 && let Some(sym) = e.referenced_symbol()
7662 && let Some(parent) = sym.parent()
7663 && parent.kind() == SymbolKind::StatementBlock
7664 && seen.insert(parent.uid())
7665 {
7666 out.push(parent);
7667 }
7668 stack.extend(live_children(&n));
7669 }
7670 out
7671}
7672
7673/// The name of a `begin`/`end` or `fork`/`join` block statement (LRM 9.3.4),
7674/// empty when unnamed.
7675///
7676/// Normally `Statement::block_symbol()`'s name. A block that failed to bind
7677/// (a statically dead arm slang could not bind, LRM 12.4) carries no symbol,
7678/// so its name would be lost and `disable blk` (LRM 9.6.2) refused. For such
7679/// a block the name is read back from the statement's own source span in the
7680/// text registered by [`set_source_text`]: either the `begin : name` form or
7681/// a `name : begin` statement label. Anything else reads as unnamed.
7682pub fn block_name(stmt: &Statement<'_>) -> String {
7683 if let Some(bs) = stmt.block_symbol() {
7684 return bs.name().to_string();
7685 }
7686 if stmt.kind() != StatementKind::Block || !contains_invalid(stmt.as_sem_node()) {
7687 return String::new();
7688 }
7689 let Some(text) = source_text_range(stmt.source_range()) else {
7690 return String::new();
7691 };
7692 let ident = |s: &str| -> Option<(String, String)> {
7693 let s = s.trim_start();
7694 let n = s
7695 .char_indices()
7696 .take_while(|&(i, c)| {
7697 c == '_' || c.is_ascii_alphabetic() || (i > 0 && (c.is_ascii_digit() || c == '$'))
7698 })
7699 .count();
7700 (n > 0).then(|| (s[..n].to_string(), s[n..].to_string()))
7701 };
7702 let Some((first, rest)) = ident(&text) else {
7703 return String::new();
7704 };
7705 if matches!(first.as_str(), "begin" | "fork") {
7706 // `begin : name`; a bare `begin` followed by a statement is unnamed.
7707 return match rest.trim_start().strip_prefix(':').and_then(ident) {
7708 Some((name, _)) => name,
7709 None => String::new(),
7710 };
7711 }
7712 // `name : begin`.
7713 match rest.trim_start().strip_prefix(':').and_then(ident) {
7714 Some((kw, _)) if matches!(kw.as_str(), "begin" | "fork") => first,
7715 _ => String::new(),
7716 }
7717}
7718
7719/// Whether any node under `node` is an `Invalid` statement or expression.
7720fn contains_invalid(node: SemNode<'_>) -> bool {
7721 let is_invalid = node
7722 .as_statement()
7723 .is_some_and(|s| s.kind() == StatementKind::Invalid)
7724 || node
7725 .as_expression()
7726 .is_some_and(|e| e.kind() == ExpressionKind::Invalid);
7727 is_invalid
7728 || node
7729 .children_with_invalid()
7730 .into_iter()
7731 .any(contains_invalid)
7732}
7733
7734/// For a plain `case` whose selector and labels are all compile-time
7735/// constants and whose arms include an `Invalid` one (a statically dead arm
7736/// slang could not bind, LRM 12.5): `Some(arm)` names the one statement that
7737/// can ever run (`None` inside: no arm matches and there is no `default`).
7738/// `None` for any case this does not apply to (non-constant, `casez`/`casex`/
7739/// `inside`, a call in a label, or no `Invalid` arm), which lowers as usual.
7740pub fn constant_case_live_arm<'d>(stmt: &Statement<'d>) -> Option<Option<Statement<'d>>> {
7741 fn has_call(e: Expression<'_>) -> bool {
7742 e.kind() == ExpressionKind::Call || e.operands().into_iter().any(has_call)
7743 }
7744 if stmt.kind() != StatementKind::Case
7745 || stmt.case_condition() != Some(sv_lang::CaseStatementCondition::Normal)
7746 || violation_check(stmt).is_some()
7747 {
7748 return None;
7749 }
7750 let items = stmt.case_items();
7751 let default = stmt.default_case();
7752 if !items
7753 .iter()
7754 .map(|i| i.stmt())
7755 .chain(default)
7756 .any(|a| contains_invalid(a.as_sem_node()))
7757 {
7758 return None;
7759 }
7760 let sel = stmt.expr()?;
7761 if has_call(sel) {
7762 return None;
7763 }
7764 let sv = sel.constant_value()?.as_i64()?;
7765 for item in &items {
7766 for label in item.expressions() {
7767 if has_call(label) {
7768 return None;
7769 }
7770 if label.constant_value()?.as_i64()? == sv {
7771 return Some(Some(item.stmt()));
7772 }
7773 }
7774 }
7775 Some(default)
7776}
7777
7778/// The fixed truth of a plain `if`'s predicate when it is a compile-time
7779/// constant with no function call in it (`if (PARAM > 0)`): `Some(taken)`
7780/// names the one arm that can ever run, so the other need not be lowered
7781/// (LRM 1800-2023 12.4; the dead arm may be legal yet unlowerable once
7782/// its constants fold, e.g. an out-of-range part-select, 11.5.1). A call is
7783/// excluded because the evaluator would run it at compile time, skipping
7784/// its side effects. `None` for any other `if`.
7785pub fn constant_if_arm(stmt: &Statement<'_>) -> Option<bool> {
7786 fn has_call(e: Expression<'_>) -> bool {
7787 e.kind() == ExpressionKind::Call || e.operands().into_iter().any(has_call)
7788 }
7789 let conditions = stmt.conditions();
7790 if stmt.kind() != StatementKind::Conditional
7791 || conditions.len() != 1
7792 || conditions[0].pattern().is_some()
7793 || violation_check(stmt).is_some()
7794 {
7795 return None;
7796 }
7797 let cond = conditions[0].expr();
7798 if has_call(cond) {
7799 return None;
7800 }
7801 cond.constant_value()
7802 .and_then(|c| c.as_i64())
7803 .map(|c| c != 0)
7804}
7805
7806/// Whether `stmt` is an `if` whose predicate is not a single plain
7807/// expression: a pattern match (`e matches p`) or an `&&&` chain (§12.6.1).
7808pub fn is_clause_if(stmt: &Statement<'_>) -> bool {
7809 if stmt.kind() != StatementKind::Conditional {
7810 return false;
7811 }
7812 let conditions = stmt.conditions();
7813 conditions.len() != 1 || conditions[0].pattern().is_some()
7814}
7815
7816/// The links an `if` dispatches over: the whole `else if` series of a
7817/// `unique`/`unique0`/`priority` `if`, else the one `if` alone (its `else`,
7818/// if any, is the `otherwise` statement).
7819pub fn if_series<'d>(head: Statement<'d>) -> Result<IfChain<'d>, &'static str> {
7820 if violation_check(&head).is_some() {
7821 return if_chain(head);
7822 }
7823 Ok(IfChain {
7824 links: vec![head],
7825 thens: vec![head.then_branch().ok_or("an `if` with no then-branch")?],
7826 otherwise: head.else_branch(),
7827 })
7828}
7829
7830/// Collects the [`IfChain`] of the `if` statement `head`.
7831pub fn if_chain<'d>(head: Statement<'d>) -> Result<IfChain<'d>, &'static str> {
7832 let mut chain = IfChain {
7833 links: Vec::new(),
7834 thens: Vec::new(),
7835 otherwise: None,
7836 };
7837 let mut link = head;
7838 loop {
7839 chain.links.push(link);
7840 chain
7841 .thens
7842 .push(link.then_branch().ok_or("an `if` with no then-branch")?);
7843 match link.else_branch() {
7844 Some(next)
7845 if next.kind() == StatementKind::Conditional
7846 && violation_check(&next).is_none() =>
7847 {
7848 link = next;
7849 }
7850 other => {
7851 chain.otherwise = other;
7852 return Ok(chain);
7853 }
7854 }
7855 }
7856}
7857
7858fn first_expression_child<'d>(tc: &TimingControl<'d>) -> Option<Expression<'d>> {
7859 tc.children().into_iter().find_map(|n| n.as_expression())
7860}
7861
7862/// Task 6 (CG-081): the AST-derived read-set of `body` -- every distinct
7863/// (by referenced-symbol identity) `NamedValue` expression `body`'s subtree
7864/// reads, excluding a plain `NamedValue` LHS of a top-level `Assignment`
7865/// (mirrors `sv-sim-codegen::process_lower::external_refs_in_segment`'s
7866/// identical "read vs write position" split, at the granularity of one
7867/// guarded statement rather than a whole `Segment`).
7868///
7869/// **Deliberately AST-only, not `AnalyzedProcedure::
7870/// implicit_event_read_sets()`:** `sv-lang` exposes exactly this oracle
7871/// (`slang::analysis::AnalyzedProcedure::getImplicitEventReadSets`), but it
7872/// needs a live `sv_lang::Analysis` handle, and no `build_cfg` call site in
7873/// this tree (`sv-sim-bin::run`, `aot.rs`, `pipeline.rs`, `frontend.rs`, this
7874/// crate's own `liveness.rs` tests) threads one through today -- adding that
7875/// parameter would be a signature change to `build_cfg` reaching every one
7876/// of those call sites, including `sv-sim-bin::run.rs`, which this wave's
7877/// binding rules forbid risking (wave-B-runtime is concurrently landing
7878/// there). This AST-only walk is a real, if narrower, substitute: an
7879/// over-approximation (a name reached only through a path this walk cannot
7880/// prove is write-only) is a safe, LRM-permitted implicit-sensitivity
7881/// superset (an extra spurious wake, never a missed one) -- never a
7882/// correctness bug the way under-approximating would be. See this file's
7883/// module doc comment's Task 6 section and this task's own deviations for
7884/// the full accounting.
7885fn implicit_comb_read_set<'d>(body: &Statement<'d>) -> Vec<Expression<'d>> {
7886 let mut order = Vec::new();
7887 let mut seen = HashSet::new();
7888 let mut stack = vec![body.as_sem_node()];
7889 while let Some(n) = stack.pop() {
7890 // A subtree slang demoted to `Invalid` inside a statically dead arm
7891 // is invisible to `children`; recover it (its reads are a harmless
7892 // superset of the live ones, LRM 9.4.2.2).
7893 if let Some(st) = n.as_statement()
7894 && st.kind() == StatementKind::Invalid
7895 {
7896 if let Some(c) = st.invalid_child() {
7897 stack.push(c.as_sem_node());
7898 }
7899 continue;
7900 }
7901 if let Some(e) = n.as_expression()
7902 && e.kind() == ExpressionKind::Invalid
7903 {
7904 if let Some(c) = e.invalid_child() {
7905 stack.push(c.as_sem_node());
7906 }
7907 continue;
7908 }
7909 if let Some(e) = n.as_expression() {
7910 if crate::symid::is_value_name(e.kind()) {
7911 if let Some(sym) = e.referenced_symbol()
7912 && seen.insert(sym.uid())
7913 {
7914 order.push(e);
7915 }
7916 continue;
7917 }
7918 if e.kind() == ExpressionKind::Assignment {
7919 // Skip only the LHS's own bare `NamedValue` node of a PLAIN
7920 // (`=`/`<=`) assignment (never a "read" for implicit-
7921 // sensitivity purposes, whether blocking or nonblocking) --
7922 // a compound lvalue (a bit-select, concatenation, ...) is
7923 // walked normally instead, so e.g. `arr[i] = x`'s index
7924 // expression `i` (and `arr`'s own identity) are still
7925 // collected, matching the "safe over-approximation" posture
7926 // above.
7927 //
7928 // A COMPOUND assignment (`+=`, `-=`, `&=`, ...) is never
7929 // desugared by slang -- `right()` holds only the RHS
7930 // operand (e.g. `a` for `q += a`), never `q + a` -- so a
7931 // bare `NamedValue` LHS genuinely is also a read there (per
7932 // IEEE 1800's implicit-sensitivity rule: a variable is
7933 // excluded from the inferred `@*` list only when it is
7934 // written but never read, which a compound assignment's LHS
7935 // is not). Detect this via `is_compound_assignment()` and
7936 // walk the LHS unconditionally in that case.
7937 let lhs_is_read = e.is_compound_assignment() == Some(true);
7938 if let Some(lhs) = e.left()
7939 && (lhs_is_read || !crate::symid::is_value_name(lhs.kind()))
7940 {
7941 stack.push(lhs.as_sem_node());
7942 }
7943 if let Some(rhs) = e.right() {
7944 stack.push(rhs.as_sem_node());
7945 }
7946 continue;
7947 }
7948 }
7949 stack.extend(n.children_with_invalid());
7950 }
7951 order
7952}
7953
7954/// LRM 6.14: a chandle "shall not be used ... in sensitivity lists or event
7955/// expressions"; slang does not diagnose it, so refuse it by name here
7956/// instead of waiting on a value that can never be observed to change.
7957fn reject_chandle_event(signal: &Expression<'_>) -> Result<(), LoweringError> {
7958 if signal.expr_type().is_some_and(|t| t.is_chandle()) {
7959 return Err(LoweringError::unsupported(
7960 "a chandle in an event expression (illegal, LRM 1800-2023 6.14)".to_string(),
7961 ));
7962 }
7963 Ok(())
7964}
7965
7966/// The operands `(memory, input_terms, output_terms)` of `stmt` when it is a
7967/// `$async$<and|or|nand|nor>$<array|plane>` call (LRM 20.16), else `None`.
7968pub fn async_pla_call<'d>(stmt: &Statement<'d>) -> Option<Vec<Expression<'d>>> {
7969 let e = stmt.expr()?;
7970 if e.kind() != ExpressionKind::Call || e.is_system_call() != Some(true) {
7971 return None;
7972 }
7973 let name = e.system_subroutine()?.name();
7974 let rest = name.strip_prefix("$async$")?;
7975 let (logic, format) = rest.split_once('$')?;
7976 (matches!(logic, "and" | "or" | "nand" | "nor") && matches!(format, "array" | "plane"))
7977 .then(|| e.operands())
7978}
7979
7980/// What an `$async$...` PLA call re-evaluates on: the personality memory and
7981/// everything the input terms read, never the output terms it writes.
7982fn async_pla_read_set<'d>(stmt: &Statement<'d>) -> Vec<Expression<'d>> {
7983 let ops = async_pla_call(stmt).unwrap_or_default();
7984 let root = |x: Expression<'d>| {
7985 let x = if x.kind() == ExpressionKind::Assignment {
7986 x.left().unwrap_or(x)
7987 } else {
7988 x
7989 };
7990 crate::symid::is_value_name(x.kind())
7991 .then(|| x.referenced_symbol())
7992 .flatten()
7993 .map(|s| (x, s.uid()))
7994 };
7995 let out_uid = ops.get(2).and_then(|o| root(*o)).map(|(_, u)| u);
7996 let mut reads: Vec<Expression<'d>> = implicit_comb_read_set(stmt)
7997 .into_iter()
7998 .filter(|r| r.referenced_symbol().map(|s| s.uid()) != out_uid)
7999 .collect();
8000 if let Some((mem, uid)) = ops.first().and_then(|o| root(*o))
8001 && !reads
8002 .iter()
8003 .any(|r| r.referenced_symbol().map(|s| s.uid()) == Some(uid.clone()))
8004 {
8005 reads.push(mem);
8006 }
8007 reads
8008}
8009
8010/// Task 3 (CG-170): extracts a `SignalEventControl`'s optional `iff`
8011/// condition. **Verified live** against the pinned `sv-lang = "=0.1.9"`
8012/// source (no `.iffCondition` accessor exists in this binding at all): per
8013/// slang's own `SignalEventControl::visitExprs` (`include/slang/ast/
8014/// TimingControl.h`: `expr.visit(visitor); if (iffCondition)
8015/// iffCondition->visit(visitor);`), which is what backs `children()`
8016/// (`CApiAst.cpp`'s generic `SemChildCollector`), a `SignalEvent`
8017/// `TimingControl` node's `children()` returns exactly 1 entry (the event
8018/// expression only) with no `iff`, or exactly 2 (event expression, then
8019/// `iff`, in that order) when one is present. Extraction is therefore
8020/// purely positional: `len() == 2`'s `[1]` is `iff`, `None` otherwise.
8021fn extract_iff<'d>(tc: &TimingControl<'d>) -> Option<Expression<'d>> {
8022 let children = tc.children();
8023 if children.len() == 2 {
8024 children[1].as_expression()
8025 } else {
8026 None
8027 }
8028}
8029
8030/// The pass and fail statements of an immediate assertion when one of them
8031/// waits (LRM 16.3: they are ordinary statements, so `#d`/`@e`/`wait` are
8032/// legal there), else `None` (the assertion stays a straight-line leaf). A
8033/// deferred assertion's action is a subroutine call that is queued (16.4), so
8034/// a waiting one is refused by name.
8035fn suspending_assertion_actions<'d>(
8036 stmt: &Statement<'d>,
8037) -> Option<(Option<Statement<'d>>, Option<Statement<'d>>)> {
8038 let pass = stmt.immediate_assertion_if_true();
8039 let fail = stmt.immediate_assertion_if_false();
8040 let waits = |s: &Option<Statement<'d>>| {
8041 s.as_ref()
8042 .is_some_and(|s| contains_suspend_trigger(s.as_sem_node()))
8043 };
8044 (waits(&pass) || waits(&fail)).then_some((pass, fail))
8045}
8046
8047/// The expression an immediate assertion checks.
8048pub fn assertion_cond<'d>(stmt: &Statement<'d>) -> Option<Expression<'d>> {
8049 stmt.as_sem_node()
8050 .children()
8051 .into_iter()
8052 .find_map(|n| n.as_expression())
8053}
8054
8055/// Whether `stmt` is an `expect` statement (LRM 1800-2023 §16.17): slang models
8056/// it as a concurrent assertion of kind `Expect`.
8057fn is_expect(stmt: &Statement<'_>) -> bool {
8058 stmt.kind() == StatementKind::ConcurrentAssertion
8059 && stmt.assertion_kind() == Some(sv_lang::AssertionKind::Expect)
8060}
8061
8062/// Generic depth-first scan (mirrors `sv-sim-codegen::module::
8063/// reject_unsupported_in_body`'s pattern) for any statement kind
8064/// [`CfgBuilder::walk_stmt`] treats as suspend-triggering, wherever it sits
8065/// inside `node`'s subtree. Used only by the "ordinary straight-line" match
8066/// arm: a construct this task does not decompose (loops, `case`, ...) is
8067/// safe to keep as one opaque leaf `Statement` only as long as none of its
8068/// descendants secretly suspends.
8069fn contains_suspend_trigger(node: SemNode<'_>) -> bool {
8070 contains_suspend_trigger_in(node, &mut std::collections::HashSet::new(), true, true)
8071}
8072
8073/// `contains_suspend_trigger`, also looking through calls to user tasks
8074/// (whose bodies are inlined when they can suspend); `visiting` breaks
8075/// recursion through recursive tasks. `by_request` says whether a
8076/// `p.await();` / `p.suspend();` statement counts: not inside a function
8077/// body, which cannot be inlined as a suspending task (a `suspend()` there
8078/// takes effect at the caller's next blocking point instead).
8079///
8080/// `disable_is_trigger`: whether a `disable label;` counts (it is a jump the
8081/// CFG decomposes, but never itself suspends the process).
8082fn contains_suspend_trigger_in(
8083 node: SemNode<'_>,
8084 visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8085 by_request: bool,
8086 disable_is_trigger: bool,
8087) -> bool {
8088 let mut stack = vec![node];
8089 while let Some(n) = stack.pop() {
8090 if let Some(e) = n.as_expression()
8091 && let Some(callee) = user_task_callee(&e)
8092 && is_suspending_virtual(&callee)
8093 {
8094 return true;
8095 }
8096 if let Some(e) = n.as_expression()
8097 && e.kind() == ExpressionKind::Assignment
8098 && e.assignment_timing().is_some()
8099 && (e.is_nonblocking() != Some(true)
8100 || e.assignment_timing()
8101 .and_then(|t| t.as_timing_control())
8102 .is_some_and(|tc| tc.kind() != TimingControlKind::Delay))
8103 {
8104 // `lhs = #d rhs;` / `lhs = @(e) rhs;` / `q <= @(e) rhs;` (LRM 9.4.5,
8105 // 10.4.2): the assignment itself waits (`walk_intra_assignment_timing`,
8106 // `walk_nba_event`), so a task holding one suspends and is inlined --
8107 // otherwise `disable task;` of another process cannot reach it (9.6.2).
8108 return true;
8109 }
8110 if let Some(e) = n.as_expression() {
8111 // An interface `extern` call runs the module-side
8112 // implementations (LRM 25.7): their bodies are what can suspend.
8113 // A call to an `extern forkjoin` task nothing implements is a
8114 // run-time error the CFG reports (`Terminator::Switch`).
8115 let callees: Vec<Symbol<'_>> = match crate::netlist::itf::call_extern_targets(&e) {
8116 Some(t) if t.impls.is_empty() => return true,
8117 Some(t) => t.impls,
8118 None => user_task_callee(&e).into_iter().collect(),
8119 };
8120 for callee in callees {
8121 // A function with an `output`/`inout`/`ref` event formal is
8122 // inlined like a suspending one (`has_event_out_formal`).
8123 if has_event_out_formal(&callee) {
8124 return true;
8125 }
8126 if let Some(body) = proc_body(&callee)
8127 && visiting.insert(callee.uid())
8128 && contains_suspend_trigger_in(
8129 body.as_sem_node(),
8130 visiting,
8131 by_request
8132 && callee.subroutine_kind() == Some(sv_lang::SubroutineKind::Task),
8133 // A function is compiled, never inlined: its own
8134 // `disable blk;` is a branch there (codegen `func`).
8135 disable_is_trigger
8136 && callee.subroutine_kind() == Some(sv_lang::SubroutineKind::Task),
8137 )
8138 {
8139 return true;
8140 }
8141 }
8142 }
8143 if let Some(s) = n.as_statement() {
8144 let kind = s.kind();
8145 if by_request
8146 && kind == StatementKind::ExpressionStatement
8147 && is_process_by_request_stmt(&s)
8148 {
8149 return true;
8150 }
8151 let is_join_block = kind == StatementKind::Block
8152 && matches!(
8153 s.block_kind(),
8154 Some(
8155 StatementBlockKind::JoinAll
8156 | StatementBlockKind::JoinAny
8157 | StatementBlockKind::JoinNone
8158 )
8159 );
8160 if is_join_block
8161 || is_expect(&s)
8162 || matches!(
8163 kind,
8164 StatementKind::Timed
8165 | StatementKind::Wait
8166 | StatementKind::WaitFork
8167 | StatementKind::WaitOrder
8168 | StatementKind::EventTrigger
8169 | StatementKind::DisableFork
8170 )
8171 // `disable label;` is a jump in the CFG (see this file's
8172 // module doc comment) -- one hidden inside an opaque
8173 // "ordinary" leaf (e.g. a `case` branch) forces that leaf to
8174 // be decomposed too.
8175 || (disable_is_trigger && kind == StatementKind::Disable)
8176 {
8177 return true;
8178 }
8179 // A static or procedural concurrent assertion (16.14) is evaluated by the
8180 // assertion engine, never by the process that reaches it: a task its
8181 // property calls from a sequence match item (16.11) runs as a thread of
8182 // its own, so it suspends no process here. Only the pass/fail action
8183 // statements run in the hosting process and can hold a suspend point.
8184 if kind == StatementKind::ConcurrentAssertion {
8185 stack.extend(
8186 live_children(&n)
8187 .into_iter()
8188 .filter(|c| c.as_statement().is_some()),
8189 );
8190 continue;
8191 }
8192 }
8193 stack.extend(live_children(&n));
8194 }
8195 false
8196}
8197
8198/// Whether `task` can call itself ([`task_reaches`]), memoized per task in
8199/// `vt` when there is one.
8200fn task_reaches_itself<'a>(task: &Symbol<'a>, vt: Option<&VirtualTasks<'a>>) -> bool {
8201 let uid = task.uid();
8202 if let Some(vt) = vt
8203 && let Some(r) = vt.self_reach.borrow().get(&uid)
8204 {
8205 return *r;
8206 }
8207 let r = task_reaches(task, &uid, vt, &mut std::collections::HashSet::new());
8208 if let Some(vt) = vt {
8209 vt.self_reach.borrow_mut().insert(uid, r);
8210 }
8211 r
8212}
8213
8214/// Whether `sub`'s body calls `target` (directly or through the user tasks
8215/// and functions it calls). A call of a virtual method reaches every
8216/// implementation `vt` knows of (LRM 1800-2023 8.20), so recursion through
8217/// dynamic dispatch (`a.f()` -> override -> `a.f()` on a child) is found.
8218fn task_reaches<'a>(
8219 sub: &Symbol<'a>,
8220 target: &crate::symid::SymbolId,
8221 vt: Option<&VirtualTasks<'a>>,
8222 visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8223) -> bool {
8224 let Some(body) = proc_body(sub) else {
8225 return false;
8226 };
8227 let mut stack = vec![body.as_sem_node()];
8228 while let Some(n) = stack.pop() {
8229 if let Some(e) = n.as_expression()
8230 && let Some(callee) = user_task_callee(&e)
8231 {
8232 let mut callees = vec![callee];
8233 // `super.m()` is not virtual (LRM 8.15): it never reaches the
8234 // overrides, so it must not be seen as recursion through them.
8235 if let Some(vt) = vt
8236 && !is_explicit_super(e)
8237 && (callee
8238 .subroutine_flags()
8239 .contains(sv_lang::MethodFlags::VIRTUAL)
8240 || callee.subroutine_override().is_some())
8241 {
8242 callees.extend(vt.implementations(&callee).into_iter().map(|(_, s)| s));
8243 }
8244 for c in callees {
8245 if c.uid() == *target {
8246 return true;
8247 }
8248 if visiting.insert(c.uid()) && task_reaches(&c, target, vt, visiting) {
8249 return true;
8250 }
8251 }
8252 }
8253 stack.extend(live_children(&n));
8254 }
8255 false
8256}
8257
8258/// Whether `node`'s subtree calls a subroutine that is not a system task.
8259fn contains_subroutine_call(node: SemNode<'_>) -> bool {
8260 let mut stack = vec![node];
8261 while let Some(n) = stack.pop() {
8262 if let Some(e) = n.as_expression()
8263 && e.kind() == ExpressionKind::Call
8264 && e.is_system_call() != Some(true)
8265 {
8266 return true;
8267 }
8268 stack.extend(live_children(&n));
8269 }
8270 false
8271}
8272
8273/// The callee of a call to a user (non-system, non-DPI) task or function.
8274fn user_task_callee<'d>(e: &Expression<'d>) -> Option<Symbol<'d>> {
8275 if e.kind() != ExpressionKind::Call || e.is_system_call() == Some(true) {
8276 return None;
8277 }
8278 let callee = e.call_subroutine()?;
8279 if callee
8280 .subroutine_flags()
8281 .contains(sv_lang::MethodFlags::DPI_IMPORT)
8282 {
8283 return None;
8284 }
8285 Some(callee)
8286}
8287
8288/// The callee of `e` when it is a call to a user task whose body can
8289/// suspend -- such a call is inlined into the calling process's CFG.
8290fn suspending_task_callee<'d>(e: &Expression<'d>) -> Option<Symbol<'d>> {
8291 let callee = user_task_callee(e)?;
8292 subroutine_can_suspend(&callee).then_some(callee)
8293}
8294
8295/// The calls to user functions that can suspend (a `fork` in the body)
8296/// under `node`, innermost (argument) calls first.
8297fn collect_suspending_function_calls<'d>(node: SemNode<'d>, out: &mut Vec<Expression<'d>>) {
8298 for c in node.children() {
8299 collect_suspending_function_calls(c, out);
8300 }
8301 if let Some(e) = node.as_expression()
8302 && let Some(callee) = user_task_callee(&e)
8303 && callee.subroutine_kind() == Some(sv_lang::SubroutineKind::Function)
8304 && subroutine_can_suspend(&callee)
8305 {
8306 out.push(e);
8307 }
8308}
8309
8310/// Whether `sub` has an `output`/`inout`/`ref` `event` formal: its handle is
8311/// copied back to the actual (LRM 1800-2023 15.5.5.1), which only an inlined
8312/// body can do, so every call to it is inlined (value functions through
8313/// `hoist_suspending_function_calls`).
8314fn has_event_out_formal(sub: &Symbol<'_>) -> bool {
8315 sub.subroutine_arguments().any(|a| {
8316 a.formal_argument_direction() != sv_lang::ArgumentDirection::In
8317 && a.value_type().is_some_and(|t| t.is_event())
8318 })
8319}
8320
8321/// The key under which the code generator finds the capture temporary that
8322/// holds the value of the inlined call `call` ([`ArgBinding::CallResult`]).
8323pub fn call_result_key(call: &Expression<'_>) -> crate::symid::SymbolId {
8324 let at = call.source_range().start;
8325 crate::symid::SymbolId::synthetic_tagged(&format!("callret.{}.{}", at.buffer, at.offset))
8326}
8327
8328/// Whether `sub`'s body (or a task it calls) contains a suspend point --
8329/// every call to such a task is inlined into the calling process's CFG
8330/// instead of being compiled as a function.
8331pub fn subroutine_can_suspend(sub: &Symbol<'_>) -> bool {
8332 if is_suspending_virtual(sub) {
8333 return true;
8334 }
8335 let Some(body) = proc_body(sub) else {
8336 return false;
8337 };
8338 // An `output`/`inout`/`ref` event formal is copied back as a handle
8339 // (LRM 1800-2023 15.5.5.1), which only an inlined task can do.
8340 if has_event_out_formal(sub) {
8341 return true;
8342 }
8343 let mut visiting = std::collections::HashSet::new();
8344 visiting.insert(sub.uid());
8345 let by_request = sub.subroutine_kind() == Some(sv_lang::SubroutineKind::Task);
8346 contains_suspend_trigger_in(body.as_sem_node(), &mut visiting, by_request, by_request)
8347 || (sub.subroutine_kind() == Some(sv_lang::SubroutineKind::Task)
8348 && task_stages_nba(sub, &mut std::collections::HashSet::new()))
8349}
8350
8351/// Whether the task `sub` (or a task it calls) runs a nonblocking assignment
8352/// (LRM 1800-2023 10.4.2). Staging an NBA needs the calling process's
8353/// scheduler handle, which a compiled function does not have, so such a task
8354/// is inlined into each calling process like a suspending one -- it just
8355/// never reaches a suspend point.
8356fn task_stages_nba(
8357 sub: &Symbol<'_>,
8358 visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8359) -> bool {
8360 let Some(body) = proc_body(sub) else {
8361 return false;
8362 };
8363 let mut stack = vec![body.as_sem_node()];
8364 while let Some(n) = stack.pop() {
8365 if let Some(e) = n.as_expression() {
8366 if e.kind() == ExpressionKind::Assignment && e.is_nonblocking() == Some(true) {
8367 return true;
8368 }
8369 if let Some(callee) = user_task_callee(&e)
8370 && callee.subroutine_kind() == Some(sv_lang::SubroutineKind::Task)
8371 && visiting.insert(callee.uid())
8372 && task_stages_nba(&callee, visiting)
8373 {
8374 return true;
8375 }
8376 }
8377 stack.extend(live_children(&n));
8378 }
8379 false
8380}
8381
8382/// The method name when `callee` is a method of the built-in `std::process`
8383/// class (LRM 9.7): the class named `process` whose scope chain reaches the
8384/// `std` package -- scope identity, as `sv-sim-codegen`'s
8385/// `is_builtin_std_class` does (a user class `process` is not it).
8386pub fn std_process_method(callee: &Symbol<'_>) -> Option<String> {
8387 std_class_method(callee, "process")
8388}
8389
8390/// The method name when `callee` is a method of the built-in `std` package
8391/// class named `class_name` (`process`, `semaphore`, `mailbox`): scope
8392/// identity, so a user class of the same name is not it.
8393pub fn std_class_method(callee: &Symbol<'_>, class_name: &str) -> Option<String> {
8394 let class = callee.parent()?;
8395 if !matches!(
8396 class.kind(),
8397 SymbolKind::ClassType | SymbolKind::GenericClassDef
8398 ) || class.name() != class_name
8399 {
8400 return None;
8401 }
8402 let mut scope = class.parent();
8403 while let Some(s) = scope {
8404 if s.kind() == SymbolKind::Package && s.name() == "std" {
8405 return Some(callee.name().to_string());
8406 }
8407 scope = s.parent();
8408 }
8409 None
8410}
8411
8412/// Whether event expression `e` names a CLASS event (LRM 1800-2023 15.5): an
8413/// instance `event` property (`m_event`, `obj.m_event`, `h[k].raised`), or an
8414/// `event` local or formal declared inside a class method. Such an event is a
8415/// runtime object (`sched::class_event`), not a row of a module's event pool.
8416/// Conversions are looked through; `null` is no class event by itself.
8417pub fn is_class_event_expr(e: &Expression<'_>) -> bool {
8418 let mut e = *e;
8419 while e.kind() == ExpressionKind::Conversion
8420 && let Some(inner) = e.conversion_operand()
8421 {
8422 e = inner;
8423 }
8424 if !e.expr_type().is_some_and(|t| t.is_event()) {
8425 return false;
8426 }
8427 let sym = if e.kind() == ExpressionKind::MemberAccess {
8428 e.member_symbol()
8429 } else if matches!(
8430 e.kind(),
8431 ExpressionKind::NamedValue | ExpressionKind::HierarchicalValue
8432 ) {
8433 e.referenced_symbol()
8434 } else {
8435 None
8436 };
8437 let Some(sym) = sym else {
8438 return false;
8439 };
8440 if sym.kind() == SymbolKind::ClassProperty {
8441 return true;
8442 }
8443 // A local/formal: the nearest enclosing subroutine is a class method.
8444 let mut scope = sym.parent();
8445 while let Some(s) = scope {
8446 if s.kind() == SymbolKind::Subroutine {
8447 return s.parent().is_some_and(|p| {
8448 matches!(
8449 p.kind(),
8450 SymbolKind::ClassType | SymbolKind::GenericClassDef
8451 )
8452 });
8453 }
8454 if matches!(
8455 s.kind(),
8456 SymbolKind::ClassType | SymbolKind::GenericClassDef
8457 ) {
8458 return false;
8459 }
8460 scope = s.parent();
8461 }
8462 false
8463}
8464
8465/// The class events `tc` waits on when it is `@e` on a class event or an
8466/// `@(e1 or e2 ...)` list with class events (no edge, no `iff` on a class
8467/// term): waits that queue through the runtime ([`is_class_event_expr`]).
8468/// Empty when `tc` waits on no class event. A list that also has signal or
8469/// module-event terms is [`class_event_wait_is_mixed`]; an edge or `iff` on a
8470/// class term is a named error.
8471pub fn class_event_wait_terms<'d>(
8472 tc: &TimingControl<'d>,
8473) -> Result<Vec<Expression<'d>>, LoweringError> {
8474 let kind = tc.kind();
8475 if kind == TimingControlKind::SignalEvent {
8476 let Some(sig) = first_expression_child(tc) else {
8477 return Ok(Vec::new());
8478 };
8479 if !is_class_event_expr(&sig) {
8480 return Ok(Vec::new());
8481 }
8482 if tc.edge() != EdgeKind::None {
8483 return Err(LoweringError::unsupported(
8484 "an edge event control on a class `event` (LRM 1800-2023 15.5)",
8485 ));
8486 }
8487 return Ok(vec![sig]);
8488 }
8489 if kind == TimingControlKind::EventList {
8490 let mut terms = Vec::new();
8491 for child in tc.children() {
8492 let Some(sub) = child.as_timing_control() else {
8493 continue;
8494 };
8495 match first_expression_child(&sub) {
8496 Some(sig)
8497 if sub.kind() == TimingControlKind::SignalEvent
8498 && is_class_event_expr(&sig) =>
8499 {
8500 if sub.edge() != EdgeKind::None || extract_iff(&sub).is_some() {
8501 return Err(LoweringError::unsupported(
8502 "an edge or `iff` term on a class `event` in an `@(a or b)` event list (LRM 1800-2023 15.5)",
8503 ));
8504 }
8505 terms.push(sig);
8506 }
8507 _ => {}
8508 }
8509 }
8510 return Ok(terms);
8511 }
8512 Ok(Vec::new())
8513}
8514
8515/// Whether `tc` is an `@(a or b)` event list that mixes class `event` terms
8516/// with signal or module-event terms (LRM 1800-2023 9.4.2.1, 15.5): such a
8517/// wait is both a class-event queue entry and a sensitivity-table wait.
8518pub fn class_event_wait_is_mixed(tc: &TimingControl<'_>) -> bool {
8519 if tc.kind() != TimingControlKind::EventList {
8520 return false;
8521 }
8522 let (mut class, mut plain) = (false, false);
8523 for child in tc.children() {
8524 let Some(sub) = child.as_timing_control() else {
8525 continue;
8526 };
8527 match first_expression_child(&sub) {
8528 Some(sig)
8529 if sub.kind() == TimingControlKind::SignalEvent && is_class_event_expr(&sig) =>
8530 {
8531 class = true;
8532 }
8533 _ => plain = true,
8534 }
8535 }
8536 class && plain
8537}
8538
8539/// Whether `stmt` is `p.await();`, `p.suspend();`, a blocking
8540/// `semaphore.get(...)` or a mailbox `put(...)`/`get(...)`/`peek(...)` as a
8541/// statement of its own (not nested in a larger expression): the `std` calls
8542/// that are a suspend point, because the caller may stop there.
8543fn is_process_by_request_stmt(stmt: &Statement<'_>) -> bool {
8544 let Some(e) = stmt.expr() else {
8545 return false;
8546 };
8547 if e.kind() != ExpressionKind::Call || e.is_system_call() == Some(true) {
8548 return false;
8549 }
8550 let Some(callee) = e.call_subroutine() else {
8551 return false;
8552 };
8553 std_process_method(&callee).is_some_and(|m| m == "await" || m == "suspend")
8554 || std_class_method(&callee, "semaphore").is_some_and(|m| m == "get")
8555 || std_class_method(&callee, "mailbox")
8556 .is_some_and(|m| matches!(m.as_str(), "put" | "get" | "peek"))
8557}
8558
8559/// Whether `stmt` is a mailbox `get(x);` / `peek(x);`: the blocking calls whose
8560/// result is delivered after the process resumes (the CFG emits the statement
8561/// again after its suspend point). A `put` finishes in its one call.
8562fn is_mailbox_two_phase_stmt(stmt: &Statement<'_>) -> bool {
8563 let Some(e) = stmt.expr() else {
8564 return false;
8565 };
8566 e.kind() == ExpressionKind::Call
8567 && e.is_system_call() != Some(true)
8568 && e.call_subroutine()
8569 .and_then(|callee| std_class_method(&callee, "mailbox"))
8570 .is_some_and(|m| m == "get" || m == "peek")
8571}
8572
8573/// Whether a call to `sub` can end the calling process from inside it: its
8574/// body, or a subroutine it calls, runs `$finish`/`$stop`/`$fatal` or a
8575/// task-form `$cast` that can fail, uses a
8576/// virtual interface (a `null` one is a fatal error, LRM 1800-2023 §25.9),
8577/// or calls a DPI import (which may call back into an export that does).
8578/// A call to a virtual method counts if ANY implementation in its override
8579/// chain can abort ([`mark_aborting_virtual`]: which override runs is only
8580/// known at run time), and a `new` expression counts if its constructor
8581/// chain or a property initializer can ([`class_construction_can_abort`]).
8582pub fn subroutine_can_abort(sub: &Symbol<'_>) -> bool {
8583 let mut visiting = std::collections::HashSet::new();
8584 can_abort_in(sub, &mut visiting)
8585}
8586
8587thread_local! {
8588 /// The uid of every virtual-method symbol (a base declaration, each
8589 /// override, an interface-class prototype) in a vtable slot at least one
8590 /// of whose implementations can abort. A call to a virtual method
8591 /// resolves statically to the BASE declaration, whose own body is empty,
8592 /// so [`can_abort_in`] cannot learn about an aborting override from the
8593 /// callee's body: codegen computes this set once, to a fixed point,
8594 /// before lowering any call site ([`mark_aborting_virtual`]).
8595 static ABORTING_VIRTUALS: std::cell::RefCell<HashSet<crate::symid::SymbolId>> =
8596 std::cell::RefCell::new(HashSet::new());
8597 /// Whether a `randomize()` call can end the process: some class's
8598 /// `pre_randomize()`/`post_randomize()` hook (which `randomize()` runs
8599 /// on the receiver or on any object it reaches, chosen by dynamic class)
8600 /// can abort ([`set_randomize_can_abort`]).
8601 static RANDOMIZE_CAN_ABORT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
8602}
8603
8604/// Empties the aborting-virtual set (and the `randomize()` flag); codegen
8605/// calls this before building a design's vtables.
8606pub fn reset_aborting_virtuals() {
8607 ABORTING_VIRTUALS.with(|s| s.borrow_mut().clear());
8608 RANDOMIZE_CAN_ABORT.with(|c| c.set(false));
8609}
8610
8611/// Declares that `randomize()` calls can abort (a `pre_randomize()` or
8612/// `post_randomize()` hook of the design can). Returns whether this changed the
8613/// flag, for a caller iterating to a fixed point.
8614pub fn set_randomize_can_abort() -> bool {
8615 RANDOMIZE_CAN_ABORT.with(|c| !c.replace(true))
8616}
8617
8618/// Whether [`set_randomize_can_abort`] has been called since the last reset.
8619pub fn randomize_can_abort() -> bool {
8620 RANDOMIZE_CAN_ABORT.with(|c| c.get())
8621}
8622
8623/// Records `uid` (a virtual method belonging to a slot with an aborting
8624/// implementation) as one whose calls can abort. Returns whether it was newly
8625/// recorded, so a caller iterating to a fixed point knows when it is done.
8626pub fn mark_aborting_virtual(uid: crate::symid::SymbolId) -> bool {
8627 ABORTING_VIRTUALS.with(|s| s.borrow_mut().insert(uid))
8628}
8629
8630fn can_abort_in(
8631 sub: &Symbol<'_>,
8632 visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8633) -> bool {
8634 if sub
8635 .subroutine_flags()
8636 .contains(sv_lang::MethodFlags::DPI_IMPORT)
8637 {
8638 return true;
8639 }
8640 if ABORTING_VIRTUALS.with(|s| s.borrow().contains(&sub.uid())) {
8641 return true;
8642 }
8643 if !visiting.insert(sub.uid()) {
8644 return false;
8645 }
8646 let Some(body) = proc_body(sub) else {
8647 return false;
8648 };
8649 if node_can_abort(body.as_sem_node(), Some(sub), visiting) {
8650 return true;
8651 }
8652 // A constructor's class-level work (property initializers, the
8653 // `extends Base(args)` call) runs with it, as part of the `new` that
8654 // invokes it.
8655 sub.subroutine_flags()
8656 .contains(sv_lang::MethodFlags::CONSTRUCTOR)
8657 && sub
8658 .parent()
8659 .and_then(|class| class.as_type())
8660 .is_some_and(|ty| class_level_can_abort(ty, visiting))
8661}
8662
8663/// Whether constructing an object of class `ty` (`new`: every constructor
8664/// from the root base class down, every level's property initializers and
8665/// `extends Base(args)` call) can end the process.
8666pub fn class_construction_can_abort(ty: sv_lang::Type<'_>) -> bool {
8667 let mut visiting = std::collections::HashSet::new();
8668 construction_can_abort(ty, &mut visiting)
8669}
8670
8671fn construction_can_abort(
8672 ty: sv_lang::Type<'_>,
8673 visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8674) -> bool {
8675 let mut level = Some(ty);
8676 while let Some(t) = level {
8677 if !t.is_class() {
8678 return false;
8679 }
8680 if class_level_can_abort(t, visiting) {
8681 return true;
8682 }
8683 if t.class_constructor()
8684 .is_some_and(|ctor| can_abort_in(&ctor, visiting))
8685 {
8686 return true;
8687 }
8688 level = t.class_base();
8689 }
8690 false
8691}
8692
8693/// Whether the work one class level does around its constructor body can
8694/// end the process: its property initializers and its `extends Base(args)`
8695/// call (the explicit-`super.new` form is part of the constructor body).
8696fn class_level_can_abort(
8697 ty: sv_lang::Type<'_>,
8698 visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8699) -> bool {
8700 let class_sym = ty.as_symbol();
8701 // `class A; A next = new; endclass` constructs itself: the guard keeps
8702 // the analysis finite (a class level is recorded once, under its own uid).
8703 if !visiting.insert(class_sym.uid()) {
8704 return false;
8705 }
8706 if let Some(call) = ty.class_base_constructor_call()
8707 && node_can_abort(call.as_sem_node(), None, visiting)
8708 {
8709 return true;
8710 }
8711 for member in class_sym.members() {
8712 if member.kind() == SymbolKind::ClassProperty
8713 && let Some(init) = member.initializer()
8714 && node_can_abort(init.as_sem_node(), None, visiting)
8715 {
8716 return true;
8717 }
8718 }
8719 false
8720}
8721
8722/// [`can_abort_in`]'s walk over one subtree. `enclosing` is the subroutine
8723/// the subtree belongs to (a `super.new(...)` finds its base class through
8724/// it).
8725fn node_can_abort<'d>(
8726 root: SemNode<'d>,
8727 enclosing: Option<&Symbol<'d>>,
8728 visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8729) -> bool {
8730 let mut stack = vec![root];
8731 while let Some(n) = stack.pop() {
8732 // `$cast(dest, src);` as a task is a run-time error when it fails
8733 // (LRM 6.24.2), ending the process like `$fatal`.
8734 if let Some(s) = n.as_statement()
8735 && s.kind() == StatementKind::ExpressionStatement
8736 && let Some(e) = s.expr()
8737 && is_failable_task_cast(&e)
8738 {
8739 return true;
8740 }
8741 if let Some(e) = n.as_expression() {
8742 if crate::netlist::vif::is_member_access(&e)
8743 || crate::netlist::vif::call_targets(&e).is_some()
8744 {
8745 return true;
8746 }
8747 if e.kind() == ExpressionKind::Call {
8748 if e.is_system_call() == Some(true) {
8749 if e.system_subroutine().is_some_and(|s| {
8750 match s.known_name_id() {
8751 KnownSystemName::Finish
8752 | KnownSystemName::Stop
8753 | KnownSystemName::Fatal => true,
8754 // `obj.randomize()` runs the class's
8755 // `pre_/post_randomize()` hooks (LRM 18.6.2).
8756 KnownSystemName::Randomize => randomize_can_abort(),
8757 _ => false,
8758 }
8759 }) {
8760 return true;
8761 }
8762 } else if let Some(callee) = e.call_subroutine() {
8763 // `p.kill()` ends the caller when `p` is the caller (or
8764 // one of its ancestors), which is only known at run time.
8765 if std_process_method(&callee).is_some_and(|m| m == "kill") {
8766 return true;
8767 }
8768 // An interface `extern` call runs its implementations.
8769 for target in crate::netlist::itf::call_callees(&e) {
8770 if can_abort_in(&target, visiting) {
8771 return true;
8772 }
8773 }
8774 }
8775 } else if e.kind() == ExpressionKind::NewClass {
8776 if let Some(ctor_call) = e.new_class_constructor_call() {
8777 stack.push(ctor_call.as_sem_node());
8778 }
8779 // `super.new(...)`'s expression type is `void`: the class
8780 // whose construction it continues is the enclosing
8781 // constructor's base. Any other `new` constructs its type.
8782 let constructed = if e.new_class_is_super_class() == Some(true) {
8783 enclosing
8784 .and_then(|sub| sub.parent())
8785 .and_then(|class| class.as_type())
8786 .and_then(|ty| ty.class_base())
8787 } else {
8788 e.expr_type().map(|t| t.canonical())
8789 };
8790 if constructed.is_some_and(|ty| construction_can_abort(ty, visiting)) {
8791 return true;
8792 }
8793 }
8794 }
8795 stack.extend(live_children(&n));
8796 }
8797 false
8798}
8799
8800/// Whether `e` is the task form of `$cast` whose destination type can reject
8801/// a value (a class handle or an enum; any other destination accepts every
8802/// source, so the call can never be the run-time error of LRM 6.24.2).
8803fn is_failable_task_cast(e: &Expression<'_>) -> bool {
8804 if e.kind() != ExpressionKind::Call
8805 || e.is_system_call() != Some(true)
8806 || !e
8807 .system_subroutine()
8808 .is_some_and(|s| s.known_name_id() == KnownSystemName::Cast)
8809 {
8810 return false;
8811 }
8812 let ops = e.operands();
8813 let Some(&dest) = ops.first() else {
8814 return false;
8815 };
8816 let dest = if dest.kind() == ExpressionKind::Assignment {
8817 dest.left().unwrap_or(dest)
8818 } else {
8819 dest
8820 };
8821 dest.expr_type()
8822 .is_some_and(|t| t.is_class() || t.is_enum())
8823}
8824
8825/// The target of an `output`/`inout` call argument: slang binds it as
8826/// `actual = <formal>`.
8827fn call_actual_lvalue<'d>(actual: Expression<'d>) -> Expression<'d> {
8828 if actual.kind() == ExpressionKind::Assignment {
8829 actual.left().unwrap_or(actual)
8830 } else {
8831 actual
8832 }
8833}
8834
8835/// The variables of the activation that forks `branch` that the branch
8836/// uses: automatic variables and inlined-task formals it does not declare
8837/// itself (static ones are the instance's signals).
8838fn captured_vars<'d>(branch: &ProcessCfg<'d>) -> Vec<Symbol<'d>> {
8839 let own: std::collections::HashSet<crate::symid::SymbolId> =
8840 super::liveness::declared_local_ids(branch);
8841 captured_in_excluding(vec![branch.body.as_sem_node()], &own)
8842}
8843
8844/// [`captured_vars`] over explicit AST roots, for a branch that declares nothing.
8845fn captured_in<'d>(roots: Vec<sv_lang::SemNode<'d>>) -> Vec<Symbol<'d>> {
8846 captured_in_excluding(roots, &std::collections::HashSet::new())
8847}
8848
8849fn captured_in_excluding<'d>(
8850 roots: Vec<sv_lang::SemNode<'d>>,
8851 own: &std::collections::HashSet<crate::symid::SymbolId>,
8852) -> Vec<Symbol<'d>> {
8853 let mut out: Vec<Symbol<'d>> = Vec::new();
8854 let mut seen = std::collections::HashSet::new();
8855 let mut stack = roots;
8856 while let Some(n) = stack.pop() {
8857 if let Some(e) = n.as_expression()
8858 && crate::symid::is_value_name(e.kind())
8859 && let Some(r) = e.referenced_symbol()
8860 {
8861 let procedural = (r.kind() == SymbolKind::Variable
8862 && r.variable_lifetime() == sv_lang::VariableLifetime::Automatic
8863 && r.parent().is_some_and(|p| {
8864 matches!(
8865 p.kind(),
8866 SymbolKind::StatementBlock | SymbolKind::Subroutine
8867 )
8868 }))
8869 || r.kind() == SymbolKind::FormalArgument;
8870 if procedural && !own.contains(&r.uid()) && seen.insert(r.uid()) {
8871 out.push(r);
8872 }
8873 }
8874 stack.extend(live_children(&n));
8875 }
8876 out
8877}
8878
8879/// The automatic variables the block statement `stmt` declares: created and
8880/// initialized each time the block is entered (LRM 1800-2023 §6.21), in
8881/// declaration order. A `for (int i = ...)` loop's variables live in the
8882/// implicit block around it, entered as the loop starts; with
8883/// `with_loop_vars` false they are left out (for a lowering that sets them
8884/// as part of the loop).
8885pub fn block_automatic_vars<'d>(stmt: &Statement<'d>, with_loop_vars: bool) -> Vec<Symbol<'d>> {
8886 if stmt.kind() != StatementKind::Block {
8887 return Vec::new();
8888 }
8889 let Some(block) = stmt.block_symbol() else {
8890 return Vec::new();
8891 };
8892 let mut loop_vars = std::collections::HashSet::new();
8893 if !with_loop_vars {
8894 let mut stack = stmt.statements();
8895 while let Some(s) = stack.pop() {
8896 if s.kind() == StatementKind::List {
8897 stack.extend(s.statements());
8898 } else if s.kind() == StatementKind::ForLoop {
8899 loop_vars.extend(s.for_loop_vars().iter().map(|v| v.uid()));
8900 }
8901 }
8902 }
8903 block
8904 .members()
8905 .filter(|m| {
8906 m.kind() == SymbolKind::Variable
8907 && m.variable_lifetime() == sv_lang::VariableLifetime::Automatic
8908 && !loop_vars.contains(&m.uid())
8909 })
8910 .collect()
8911}
8912
8913/// The prefix of the names sv-sim gives the blocks it introduces into a
8914/// design (`randsequence`'s expansion): they are not part of any
8915/// hierarchical name the design prints.
8916pub const INTERNAL_NAME_PREFIX: &str = "__sv_sim_";
8917
8918/// `sym`'s hierarchical name as the design sees it (`%m`, a message's
8919/// scope): the blocks sv-sim introduced ([`INTERNAL_NAME_PREFIX`]) left out.
8920pub fn shown_path(sym: &Symbol<'_>) -> String {
8921 let path = sym.hierarchical_path();
8922 if !path.contains(INTERNAL_NAME_PREFIX) {
8923 return path;
8924 }
8925 let mut out = String::with_capacity(path.len());
8926 let mut rest = path.as_str();
8927 while let Some(i) = rest.find(INTERNAL_NAME_PREFIX) {
8928 let at_segment = i == 0 || rest.as_bytes()[i - 1] == b'.';
8929 if !at_segment {
8930 out.push_str(&rest[..i + INTERNAL_NAME_PREFIX.len()]);
8931 rest = &rest[i + INTERNAL_NAME_PREFIX.len()..];
8932 continue;
8933 }
8934 // Drop the segment and the `.` before it (or after it, at the start).
8935 let end = rest[i..].find('.').map_or(rest.len(), |e| i + e);
8936 if i > 0 {
8937 out.push_str(&rest[..i - 1]);
8938 rest = &rest[end..];
8939 } else {
8940 rest = rest[end..].strip_prefix('.').unwrap_or(&rest[end..]);
8941 }
8942 }
8943 out.push_str(rest);
8944 out
8945}
8946
8947/// Whether a variable of type `ty` can hold a class handle or a queue /
8948/// dynamic / associative array handle (directly or in a struct field): the
8949/// variables whose block exit owes a release. A fixed-size array of handles
8950/// is not covered (its elements are released on overwrite only).
8951pub fn may_hold_handles(ty: &sv_lang::Type<'_>) -> bool {
8952 if ty.is_class() || crate::state::storage::is_collection(ty) {
8953 return true;
8954 }
8955 ty.is_unpacked_struct()
8956 && ty
8957 .fields()
8958 .any(|f| f.value_type().is_some_and(|t| may_hold_handles(&t)))
8959}
8960
8961/// Whether `var` is an automatic variable a block declares (initialized on
8962/// entry to that block, [`block_automatic_vars`]).
8963pub fn is_automatic_block_var(var: &Symbol<'_>) -> bool {
8964 var.variable_lifetime() == sv_lang::VariableLifetime::Automatic
8965 && var
8966 .parent()
8967 .is_some_and(|p| p.kind() == SymbolKind::StatementBlock)
8968}
8969
8970/// A task's own variables: members of the task scope (not its formals or
8971/// result variable) and of every block inside its body.
8972fn task_locals<'d>(task: &Symbol<'d>) -> Vec<Symbol<'d>> {
8973 let formal_ids: std::collections::HashSet<crate::symid::SymbolId> =
8974 task.subroutine_arguments().map(|a| a.uid()).collect();
8975 let ret_id = task.subroutine_return_val_var().map(|r| r.uid());
8976 let mut out = Vec::new();
8977 let mut seen = std::collections::HashSet::new();
8978 let mut take = |m: Symbol<'d>, out: &mut Vec<Symbol<'d>>| {
8979 let id = m.uid();
8980 if m.kind() == SymbolKind::Variable
8981 && !formal_ids.contains(&id)
8982 && Some(&id) != ret_id.as_ref()
8983 && seen.insert(id)
8984 {
8985 out.push(m);
8986 }
8987 };
8988 for m in task.members() {
8989 take(m, &mut out);
8990 }
8991 if let Some(body) = proc_body(task) {
8992 let mut stack = vec![body.as_sem_node()];
8993 while let Some(n) = stack.pop() {
8994 if let Some(s) = n.as_statement() {
8995 for block in block_scopes(&s) {
8996 for m in block.members() {
8997 take(m, &mut out);
8998 }
8999 }
9000 }
9001 let mut kids = live_children(&n);
9002 kids.reverse();
9003 stack.extend(kids);
9004 }
9005 }
9006 out
9007}
9008
9009// --- Segment IR (PR-041, PR-042, PR-043; SegId numbering CG-098, CG-099) ---
9010
9011/// Task S3b's real liveness/frame-layout shape now lives in
9012/// `process::liveness` (`FrameLayout`/`FrameSlotDesc`/`FrameSlotSource`) --
9013/// re-exported here under this task's original name so every existing call
9014/// site (this file's own `lower_to_segments`, `tests/process_cfg.rs`) keeps
9015/// working unchanged.
9016pub use super::liveness::FrameLayout;
9017use super::liveness::FrameSlotSource;
9018
9019/// The codegen-facing, already-liveness-annotated lowering shape (PR-042).
9020/// `live_in`/`live_out` are `FrameLayout::slots` indices (Task S3b: see
9021/// `lower_to_segments`'s doc comment for exactly which slots those are).
9022#[derive(Debug, Clone)]
9023pub struct Segment<'d> {
9024 pub id: SegId,
9025 pub live_in: Vec<u32>,
9026 /// Mirrors [`BasicBlock::bindings`] 1:1 -- run before `body`.
9027 pub bindings: Vec<ArgBinding<'d>>,
9028 /// Mirrors [`BasicBlock::this_var`] 1:1.
9029 pub this_var: Option<Symbol<'d>>,
9030 /// Mirrors [`BasicBlock::ref_aliases`] 1:1.
9031 pub ref_aliases: Vec<(Symbol<'d>, Symbol<'d>)>,
9032 pub body: Vec<Statement<'d>>,
9033 /// Mirrors [`BasicBlock::probes`] 1:1 -- indices are into `body`.
9034 pub probes: Vec<(usize, Statement<'d>)>,
9035 /// Mirrors [`BasicBlock::disable_probes`] 1:1 -- indices are into `body`.
9036 pub disable_probes: Vec<(usize, Statement<'d>)>,
9037 /// Mirrors [`BasicBlock::frame_probes`] 1:1 -- indices are into `body`.
9038 pub frame_probes: Vec<(usize, FrameProbe<'d>)>,
9039 /// Mirrors [`BasicBlock::block_events`] 1:1 -- indices are into `body`.
9040 pub block_events: Vec<(usize, Symbol<'d>, bool)>,
9041 /// Mirrors [`BasicBlock::stmt_scopes`] 1:1 -- indices are into `body`.
9042 pub stmt_scopes: Vec<(usize, Symbol<'d>)>,
9043 /// Mirrors [`BasicBlock::step_exprs`] 1:1 -- see its doc comment.
9044 pub step_exprs: Vec<Expression<'d>>,
9045 /// Mirrors [`BasicBlock::repeat_init`] 1:1 -- see its doc comment.
9046 pub repeat_init: Option<(RepeatCounterId, RepeatCount<'d>)>,
9047 /// Mirrors [`BasicBlock::repeat_decrement`] 1:1 -- see its doc comment.
9048 pub repeat_decrement: Option<RepeatCounterId>,
9049 /// Mirrors [`BasicBlock::expect_resume`] 1:1 -- see its doc comment.
9050 pub expect_resume: bool,
9051 pub terminator: SegTerminator<'d>,
9052}
9053
9054/// PR-043: covers every [`Terminator`] variant with no lossy fallback (see
9055/// [`lower_to_segments`]'s 1:1 mapping).
9056#[derive(Debug, Clone)]
9057pub enum SegTerminator<'d> {
9058 Goto(SegId),
9059 CondGoto {
9060 cond: Expression<'d>,
9061 t: SegId,
9062 f: SegId,
9063 },
9064 Suspend {
9065 point: SuspendPoint<'d>,
9066 resume: SegId,
9067 live_out: Vec<u32>,
9068 },
9069 End {
9070 loop_back: Option<SegId>,
9071 },
9072 /// The lowering of [`Terminator::Switch`].
9073 SwitchGoto {
9074 on: SwitchOn<'d>,
9075 arms: Vec<SegId>,
9076 default: SegId,
9077 report: Option<sv_lang::SourceLoc>,
9078 },
9079 /// R-run Task 8: the lossless lowering of [`Terminator::RepeatCheck`] --
9080 /// see that variant's own doc comment for why a `repeat(N)` loop's
9081 /// per-iteration decision cannot be represented as an ordinary
9082 /// `CondGoto` (no `cond: Expression` exists for "compiler-synthesized
9083 /// counter != 0"). **Deviation from this file's own module doc comment**
9084 /// (Task 1's "frozen, codegen-exhausted `SegTerminator`" policy): that
9085 /// policy was written to rule out `ForkJoin` specifically (which has no
9086 /// honest `SegTerminator` shape at all, ever, this wave); a suspend
9087 /// nested inside a `repeat` body is a genuinely new kind of branch
9088 /// condition this crate had no way to construct before this task, not a
9089 /// reachability gap in the pre-existing four variants -- recorded in
9090 /// this task's own deviations.
9091 CondGotoCounter {
9092 counter: RepeatCounterId,
9093 t: SegId,
9094 f: SegId,
9095 },
9096 /// R-run Task 17 (PR-047/PR-061): the lossless lowering of
9097 /// [`Terminator::ForkJoin`] -- a real, new `SegTerminator` shape, added
9098 /// for the identical reason `CondGotoCounter` above already documents a
9099 /// deviation from this file's own "frozen `SegTerminator`" policy note:
9100 /// that note was written to rule out `ForkJoin` specifically because
9101 /// NOTHING had implemented fork/join scheduling yet; this task is that
9102 /// implementation for `StatementBlockKind::JoinAll` (codegen enforces
9103 /// the JoinAll-only scope with a named `LoweringError` for `JoinAny`/
9104 /// `JoinNone` -- this type carries `kind` through losslessly so codegen
9105 /// has it to check, rather than this crate silently narrowing what it
9106 /// represents). `branches` is exactly [`Terminator::ForkJoin`]'s own
9107 /// field, carried through unchanged (each branch is still a full,
9108 /// independently suspend-capable nested [`ProcessCfg`] -- codegen
9109 /// recursively lowers each one into its own real compiled kernel).
9110 /// `join_blk`/`live_out` mirror `Suspend`'s own `resume`/`live_out`
9111 /// fields exactly: a fork/join site suspends the calling process (until
9112 /// every spawned child completes, for `JoinAll`) just as surely as a
9113 /// `Suspend` terminator does, so the same permanent-frame-slot
9114 /// liveness/writeback discipline applies -- see [`lower_to_segments`]'s
9115 /// own fill rule.
9116 ForkJoin {
9117 kind: StatementBlockKind,
9118 branches: Vec<ProcessCfg<'d>>,
9119 join_blk: SegId,
9120 live_out: Vec<u32>,
9121 },
9122 /// The lowering of [`Terminator::Assertion`].
9123 AssertGoto {
9124 stmt: Statement<'d>,
9125 pass: SegId,
9126 fail: SegId,
9127 done: SegId,
9128 },
9129 /// The lowering of [`Terminator::Case`]: goes to `arms[i]` for the first
9130 /// item of `stmt` whose labels match its selector, else `default`.
9131 CaseGoto {
9132 stmt: Statement<'d>,
9133 arms: Vec<SegId>,
9134 default: SegId,
9135 },
9136}
9137
9138/// PR-041/042/043: a 1:1, source-order-stable (CG-098, CG-099) lowering of
9139/// each `ProcessCfg` [`BasicBlock`] into a [`Segment`] -- `blocks[i]` always
9140/// becomes `SegId(i)`, so segment numbering is fixed the moment `build_cfg`
9141/// allocates its blocks, well before this function (or any LLVM `Context`)
9142/// ever runs.
9143///
9144/// **Task S3b's `live_in`/`live_out` fill rule:** every `FrameLayout` slot
9145/// with source `FrameSlotSource::DeclaredVar` is a *permanent* slot
9146/// (PR-017/018 -- valid for the process's entire lifetime, never
9147/// added/removed by liveness), so its index belongs in `live_in`/`live_out`
9148/// at every segment, unconditionally. A `FrameSlotSource::Temporary` slot's
9149/// index would additionally need to appear only where
9150/// `liveness::compute_liveness`'s own backward fixpoint says it is actually
9151/// live; this task's own restricted CFG-level IR never has a positive source
9152/// for a `Temporary` slot at all (one is only ever minted once CODEGEN's
9153/// Task S3c starts decomposing expressions into named SSA values with no
9154/// corresponding `sv_lang` symbol), so `frame.slots` here is, for every
9155/// fixture this task can construct, exactly the `DeclaredVar` set -- see
9156/// `liveness.rs`'s own module doc comment for the full reasoning. A future
9157/// Task S3c revision of this function additionally intersects
9158/// `compute_liveness`'s per-segment result with whatever `Temporary` slots
9159/// exist by then; this rule does not change for `DeclaredVar` slots.
9160pub fn lower_to_segments<'d>(cfg: &ProcessCfg<'d>, frame: &FrameLayout) -> Vec<Segment<'d>> {
9161 // R-run Task 8: a `repeat` loop's synthetic counter is ALSO permanent --
9162 // it must survive across a suspend/resume call boundary exactly like a
9163 // declared local (see `FrameSlotSource::SyntheticCounter`'s own doc
9164 // comment), so its slot index belongs in `live_in`/`live_out` at every
9165 // segment unconditionally too, same rule as `DeclaredVar`.
9166 let permanent: Vec<u32> = frame
9167 .slots
9168 .iter()
9169 .enumerate()
9170 .filter(|(_, s)| {
9171 matches!(
9172 s.source,
9173 FrameSlotSource::DeclaredVar(_)
9174 | FrameSlotSource::SyntheticCounter(_)
9175 | FrameSlotSource::Temporary(_)
9176 )
9177 })
9178 .map(|(i, _)| i as u32)
9179 .collect();
9180
9181 cfg.blocks
9182 .iter()
9183 .enumerate()
9184 .map(|(i, b)| Segment {
9185 id: SegId(i as u32),
9186 live_in: permanent.clone(),
9187 bindings: b.bindings.clone(),
9188 this_var: b.this_var,
9189 ref_aliases: b.ref_aliases.clone(),
9190 body: b.stmts.clone(),
9191 probes: b.probes.clone(),
9192 disable_probes: b.disable_probes.clone(),
9193 frame_probes: b.frame_probes.clone(),
9194 block_events: b.block_events.clone(),
9195 stmt_scopes: b.stmt_scopes.clone(),
9196 step_exprs: b.step_exprs.clone(),
9197 repeat_init: b.repeat_init.clone(),
9198 repeat_decrement: b.repeat_decrement,
9199 expect_resume: b.expect_resume,
9200 terminator: lower_terminator(&b.terminator, &permanent),
9201 })
9202 .collect()
9203}
9204
9205/// Task 1: `true` iff any block in `cfg` closes with `Terminator::ForkJoin`
9206/// -- fork/join has no runtime scheduling semantics this wave (`docs/
9207/// design/process.md` §7: "construction only, never scheduled"), so
9208/// [`lower_to_segments`]/`lower_terminator` cannot honestly produce a
9209/// `SegTerminator` for one (there is no shape in that frozen, codegen-
9210/// exhausted enum for "spawn N branches" -- see this file's module doc
9211/// comment's `SegTerminator`-freeze note). Every real caller (this crate's
9212/// own `sv-sim-bin::run`) must check this and fail loudly *before* calling
9213/// `lower_to_segments` on a CFG that might contain one; `lower_terminator`
9214/// itself still documents and asserts the precondition (`unreachable!`) as
9215/// a caller-discipline internal invariant, exactly like `finish()`'s own
9216/// "every block must be closed" `.expect()` -- not a condition reachable
9217/// from malformed SV source once a caller has checked this predicate first.
9218pub fn contains_unschedulable_fork_join(cfg: &ProcessCfg<'_>) -> bool {
9219 cfg.blocks
9220 .iter()
9221 .any(|b| matches!(b.terminator, Terminator::ForkJoin { .. }))
9222}
9223
9224/// R-run Task 17 (PR-047/PR-061): `true` iff any block in `cfg` closes with
9225/// a `Terminator::ForkJoin` whose `kind` is `JoinAny`/`JoinNone` -- the two
9226/// fork/join shapes this task's own scope note ("Scope for this task:
9227/// `JoinAll` only") leaves unimplemented at the codegen layer.
9228/// **Deliberately narrower than [`contains_unschedulable_fork_join`]
9229/// above, which this function does NOT replace or modify** (that
9230/// function's existing callers -- `sv-sim-bin::aot`, which this task does
9231/// not extend to support ANY fork/join shape -- keep their original,
9232/// unchanged "reject every fork/join" behavior unaffected): a `JoinAll`
9233/// fork/join now has a real [`SegTerminator::ForkJoin`] shape
9234/// [`lower_to_segments`] can produce, so a caller that DOES support it
9235/// (`sv-sim-bin::run`) needs a predicate that only rejects the two kinds
9236/// still genuinely unsupported, letting `JoinAll` flow through to
9237/// `lower_to_segments`/codegen instead of being rejected pre-emptively.
9238pub fn contains_unsupported_fork_join_kind(cfg: &ProcessCfg<'_>) -> bool {
9239 cfg.blocks.iter().any(|b| {
9240 matches!(
9241 b.terminator,
9242 Terminator::ForkJoin {
9243 kind: StatementBlockKind::JoinAny | StatementBlockKind::JoinNone,
9244 ..
9245 }
9246 )
9247 })
9248}
9249
9250fn lower_terminator<'d>(t: &Terminator<'d>, permanent: &[u32]) -> SegTerminator<'d> {
9251 match t {
9252 Terminator::Fallthrough(b) => SegTerminator::Goto(SegId(b.0)),
9253 Terminator::Switch {
9254 on,
9255 arms,
9256 default_blk,
9257 report,
9258 } => SegTerminator::SwitchGoto {
9259 on: on.clone(),
9260 arms: arms.iter().map(|b| SegId(b.0)).collect(),
9261 default: SegId(default_blk.0),
9262 report: *report,
9263 },
9264 Terminator::Branch {
9265 cond,
9266 then_blk,
9267 else_blk,
9268 } => SegTerminator::CondGoto {
9269 cond: *cond,
9270 t: SegId(then_blk.0),
9271 f: SegId(else_blk.0),
9272 },
9273 Terminator::Suspend { point, resume_blk } => SegTerminator::Suspend {
9274 point: point.clone(),
9275 resume: SegId(resume_blk.0),
9276 live_out: permanent.to_vec(),
9277 },
9278 Terminator::End { loop_back } => SegTerminator::End {
9279 loop_back: loop_back.map(|b| SegId(b.0)),
9280 },
9281 // Task 1: `Loop` lowers losslessly to `SegTerminator::Goto` -- see
9282 // the module doc comment's `SegTerminator`-freeze note. Its real
9283 // per-iteration decision already lives on whichever block `entry`
9284 // names (a real `Branch`, or `Fallthrough` for `forever`), so
9285 // nothing this enum could represent is lost.
9286 Terminator::Loop { entry, .. } => SegTerminator::Goto(SegId(entry.0)),
9287 // R-run Task 8: see `SegTerminator::CondGotoCounter`'s own doc
9288 // comment for why this is a real, new lowering (not a lossless
9289 // `Goto`/`CondGoto` reuse the way `Loop` above is).
9290 Terminator::RepeatCheck {
9291 counter,
9292 then_blk,
9293 else_blk,
9294 } => SegTerminator::CondGotoCounter {
9295 counter: *counter,
9296 t: SegId(then_blk.0),
9297 f: SegId(else_blk.0),
9298 },
9299 // R-run Task 17: no longer unreachable -- see `SegTerminator::
9300 // ForkJoin`'s own doc comment. Lossless: `branches`/`kind` carry
9301 // straight through, `join_blk`/`live_out` get the exact same
9302 // treatment `Suspend`'s `resume`/`live_out` get above (a fork/join
9303 // site is a real suspend point too).
9304 Terminator::ForkJoin {
9305 kind,
9306 branches,
9307 join_blk,
9308 } => SegTerminator::ForkJoin {
9309 kind: *kind,
9310 branches: branches.clone(),
9311 join_blk: SegId(join_blk.0),
9312 live_out: permanent.to_vec(),
9313 },
9314 Terminator::Assertion {
9315 stmt,
9316 pass_blk,
9317 fail_blk,
9318 done_blk,
9319 } => SegTerminator::AssertGoto {
9320 stmt: *stmt,
9321 pass: SegId(pass_blk.0),
9322 fail: SegId(fail_blk.0),
9323 done: SegId(done_blk.0),
9324 },
9325 Terminator::Case {
9326 stmt,
9327 arms,
9328 default_blk,
9329 } => SegTerminator::CaseGoto {
9330 stmt: *stmt,
9331 arms: arms.iter().map(|b| SegId(b.0)).collect(),
9332 default: SegId(default_blk.0),
9333 },
9334 }
9335}
9336
9337// --- PR-074: cross-check against AnalyzedProcedure::timing_controls() ---
9338
9339/// Counts every `Timed` statement anywhere in `body`'s subtree, independent
9340/// of whether `build_cfg` would actually accept each one (a rejected
9341/// `TimingControlKind` is still a `Timed` statement) -- this subsystem's own
9342/// "how many suspend points did the tree walk find" half of PR-074's
9343/// cross-check.
9344pub fn count_timed_statements(body: &Statement<'_>) -> usize {
9345 let mut count = 0usize;
9346 let mut stack = vec![body.as_sem_node()];
9347 while let Some(n) = stack.pop() {
9348 if let Some(s) = n.as_statement()
9349 && s.kind() == StatementKind::Timed
9350 {
9351 count += 1;
9352 }
9353 stack.extend(live_children(&n));
9354 }
9355 count
9356}
9357
9358/// PR-074: "the subsystem's own suspend-point tree walk must be cross-checked
9359/// against `AnalyzedProcedure::timing_controls()`; a mismatch ... MUST be a
9360/// hard-fail bug, not a warning." Factored as a pure comparator over two
9361/// counts (rather than requiring every caller to thread a live
9362/// `sv_lang::Analysis` through) so a test can fault-inject a mismatch
9363/// directly -- see `cross_check_hard_fails_on_a_fault_injected_mismatch` in
9364/// `tests/process_cfg.rs`. The real call site passes
9365/// `own_walk_count = count_timed_statements(body)` and
9366/// `analyzed_count = analyzed_procedure.timing_controls().count()`.
9367pub fn check_suspend_count_matches(
9368 own_walk_count: usize,
9369 analyzed_count: usize,
9370) -> Result<(), LoweringError> {
9371 if own_walk_count == analyzed_count {
9372 Ok(())
9373 } else {
9374 Err(LoweringError::malformed(format!(
9375 "PR-074 suspend-point cross-check failed: this subsystem's own tree walk found \
9376 {own_walk_count} Timed statement(s), but AnalyzedProcedure::timing_controls() \
9377 found {analyzed_count} -- this is a hard bug (a missed or double-counted \
9378 suspend point), never a warning"
9379 )))
9380 }
9381}