vj_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//! `vj-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 `vj-codegen::error::LoweringError`: this crate must
146/// never depend on `vj-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 `vj-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 /// (`vj-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 (`vj-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 /// `vj::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 (`vj-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/vj-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/vj-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 /// `vj-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 `vj-sim`'s own tests and the JIT+run
434/// matrix in `vj-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 `vj-sim`'s own meta-test and
481/// `vj-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/// `vj-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/vj-sim/tests/frame_roundtrip_matrix.rs`, `crates/
492/// vj-codegen/tests/frame_roundtrip_matrix.rs`, this file) can close
493/// without expanding into `vj-codegen::process_lower`'s own `Files`
494/// scope. `vj-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 /// Whether term `k` of the event wait just before matched (bit `k` of
764 /// `sv_rt_fired_mask`): 1 when it did, else 0. A class event queues every
765 /// term of an `@(a iff x or b iff y)` list on its own, and the resume
766 /// judges each fired term's `iff` (LRM 1800-2023 9.4.2.3, 15.5).
767 FiredBit(u32),
768 /// After a mixed `@(class_event or signal ...)` wait: whether class term
769 /// `k` of the list (its kth class event) woke the process (`Some(k)`), or
770 /// any class term did (`None`); 0 when a signal term woke it. The class
771 /// side keeps its own record (`sched::class_event::fired_bit`) because a
772 /// signal term's index would share the fired-term mask with it.
773 ClassFired(Option<u32>),
774 /// A virtual interface's instance id minus one (`netlist::vif`): an
775 /// interface task called through it.
776 VifId(Expression<'d>),
777 /// A `randcase`'s item, picked at random by the items' weights (none
778 /// when every weight is 0).
779 RandCase(Statement<'d>),
780 /// The arm whose class template ids hold the dynamic class of the object
781 /// `receiver` names (the current object when `None`): a virtual class
782 /// task that can suspend, one inlined override per arm (LRM 8.20).
783 ObjClass {
784 receiver: Option<Expression<'d>>,
785 ids: Vec<Vec<u32>>,
786 },
787 /// No arm is ever taken: the switch only reports its error (`what` is the
788 /// message) and continues at the default -- a call to an `extern forkjoin`
789 /// task no module implements (LRM 25.7.5).
790 NoTarget { what: String },
791 /// The arm of the named block (`labels[i]`, innermost first) that the
792 /// subroutine call just before disabled with `disable <block>;` from
793 /// inside a function (LRM 9.6.2, 35.9), else the default
794 /// (`sv_rt_dpi_fn_disable_index`).
795 FnDisable { labels: Vec<String> },
796}
797
798impl std::fmt::Debug for SwitchOn<'_> {
799 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
800 match self {
801 SwitchOn::NoTarget { what } => write!(f, "NoTarget({what})"),
802 SwitchOn::FnDisable { labels } => write!(f, "FnDisable({labels:?})"),
803 SwitchOn::FiredTerm => write!(f, "FiredTerm"),
804 SwitchOn::FiredBit(k) => write!(f, "FiredBit({k})"),
805 SwitchOn::ClassFired(k) => write!(f, "ClassFired({k:?})"),
806 SwitchOn::VifId(e) => write!(f, "VifId({:?})", e.kind()),
807 SwitchOn::RandCase(_) => write!(f, "RandCase"),
808 SwitchOn::ObjClass { ids, .. } => write!(f, "ObjClass({ids:?})"),
809 }
810 }
811}
812
813impl<'d> SwitchOn<'d> {
814 /// Every expression the selection reads.
815 pub fn reads(&self) -> Vec<Expression<'d>> {
816 match self {
817 SwitchOn::FiredTerm
818 | SwitchOn::FiredBit(_)
819 | SwitchOn::ClassFired(_)
820 | SwitchOn::NoTarget { .. }
821 | SwitchOn::FnDisable { .. } => Vec::new(),
822 SwitchOn::VifId(e) => vec![*e],
823 SwitchOn::RandCase(s) => s.randcase_items().iter().map(|i| i.expr()).collect(),
824 SwitchOn::ObjClass { receiver, .. } => receiver.iter().copied().collect(),
825 }
826 }
827}
828
829/// One enclosing `foreach` dimension on the way to a nested collection
830/// ([`RepeatCount::NestedSize`]): its index variable and, for a fixed-size
831/// dimension, its `(left, right)` bounds (`None`: a queue / dynamic array
832/// dimension, indexed from 0).
833#[derive(Debug, Clone, Copy)]
834pub struct NestStep<'d> {
835 pub var: Symbol<'d>,
836 pub fixed: Option<(i32, i32)>,
837 /// The dimension is an associative array: `var` holds a key, not a
838 /// position.
839 pub assoc: bool,
840}
841
842/// A counted loop's iteration count: a `repeat (N)` loop's count
843/// expression, a `foreach` loop's static element count, or the number of
844/// elements a queue or dynamic array has when its `foreach` starts.
845#[derive(Debug, Clone)]
846pub enum RepeatCount<'d> {
847 Expr(Expression<'d>),
848 Const(u64),
849 /// The size of this queue / dynamic array expression.
850 Size(Expression<'d>),
851 /// The size of the queue / dynamic array reached from `arr` by the
852 /// enclosing `foreach` indices `path` (`a[i][j]` of `foreach (a[i, j,
853 /// k])` over `int a[2][3][]`), read when the loop is entered.
854 NestedSize {
855 arr: Expression<'d>,
856 path: Vec<NestStep<'d>>,
857 },
858 /// A count the forking process already evaluated: the counter slot is
859 /// copied in at the spawn ([`InheritedSlot::Counter`]), never
860 /// initialized here. `0` is the count expression, which sizes the slot.
861 Inherited(Expression<'d>),
862}
863
864impl<'d> RepeatCount<'d> {
865 /// The expression the count is computed from, if the count is not a
866 /// constant (for `Size`, the collection).
867 pub fn expr(&self) -> Option<Expression<'d>> {
868 match self {
869 RepeatCount::Expr(e) | RepeatCount::Size(e) => Some(*e),
870 RepeatCount::NestedSize { arr, .. } => Some(*arr),
871 RepeatCount::Const(_) | RepeatCount::Inherited(_) => None,
872 }
873 }
874}
875
876/// A straight-line run of non-suspending statements ending in exactly one
877/// [`Terminator`] (PR-008).
878#[derive(Debug, Clone)]
879pub struct BasicBlock<'d> {
880 /// Inlined-task argument transfers, run before `stmts` (see
881 /// [`ArgBinding`]).
882 pub bindings: Vec<ArgBinding<'d>>,
883 /// The `this` of the innermost class-method task inlined at this block
884 /// (its body's blocks; its binding and continuation blocks belong to the
885 /// caller): what an implicit-`this` member access or method call in the
886 /// block resolves against when the process itself is not a method.
887 pub this_var: Option<Symbol<'d>>,
888 /// The `ref` formals of the tasks inlined at this block that name a
889 /// static variable or net of their call (LRM 13.5.2), with that storage:
890 /// the formal reads and writes the actual itself in this block, so one
891 /// process may bind the same formal to different actuals per call.
892 pub ref_aliases: Vec<(Symbol<'d>, Symbol<'d>)>,
893 pub stmts: Vec<Statement<'d>>,
894 /// VPI `cbStmt` probe points (IEEE 1800-2023 38.36.1): `(i, s)` means the
895 /// statement `s` is about to execute and the probe runs just before
896 /// `stmts[i]` (`i == stmts.len()` is after the last one, still before
897 /// `step_exprs` and the terminator). Only statements this CFG decomposes
898 /// (or keeps as a leaf, which codegen then does not probe again) appear
899 /// here; codegen emits a probe for them only when a VPI library is loaded.
900 pub probes: Vec<(usize, Statement<'d>)>,
901 /// VPI `cbDisable` probe points (38.36.1): `(i, s)` means the `disable`
902 /// statement `s`, which this CFG turned into a jump out of the named block
903 /// it names (so it is not in `stmts`), has taken effect; the probe runs
904 /// where `probes` at index `i` do, after them.
905 pub disable_probes: Vec<(usize, Statement<'d>)>,
906 /// VPI frame probe points (37.43) of the blocking tasks inlined into this
907 /// CFG: they run where `probes` at the same index do, after them (and
908 /// after any `disable_probes`).
909 pub frame_probes: Vec<(usize, FrameProbe<'d>)>,
910 /// Covergroup block events (LRM 19.3 `@@(begin t)` / `@@(end t)`) of the
911 /// blocking tasks inlined into this CFG: `(i, task, is_end)` signals the
912 /// begin (or end) of `task`'s activation where `probes` at index `i` run.
913 /// Codegen emits only those whose task a covergroup names.
914 pub block_events: Vec<(usize, Symbol<'d>, bool)>,
915 /// `(i, subroutine)`: `stmts[i]` was written in the body of the inlined `subroutine`, whose
916 /// time unit its delays (`q <= #d v`) are in (LRM 3.14.2.1). Absent for the process's own.
917 pub stmt_scopes: Vec<(usize, Symbol<'d>)>,
918 /// A `for` loop's per-iteration step expressions (e.g. `i++`), run after
919 /// `stmts` and before `terminator` fires (Task 1, `walk_for_loop`).
920 /// Empty for every block that is not a `for` loop's step block. A
921 /// separate field, not folded into `stmts: Vec<Statement<'d>>`, because
922 /// `Statement::for_loop_steps()` returns bare `Expression<'d>`s (`i++`
923 /// is an expression in this grammar position, never a `Statement` --
924 /// confirmed against the pinned `sv-lang`=0.1.9 binding, which has no
925 /// expression-to-statement wrapper) and this crate never fabricates AST
926 /// nodes. Every slot this task's `for` fixture writes through
927 /// (`i` itself) is a `FrameSlotSource::DeclaredVar` permanent slot
928 /// (`lower_to_segments`'s own doc comment), so omitting these from
929 /// `liveness::compute_liveness`'s backward read/write walk does not
930 /// under-approximate any frame-slot's live range -- codegen consumption
931 /// of this field is future work, tracked in this task's deviations, not
932 /// a silent drop (the expressions are still here, in the CFG, for
933 /// whichever pass reads `step_exprs` next).
934 pub step_exprs: Vec<Expression<'d>>,
935 /// R-run Task 8: `Some((counter, count_expr))` iff this block is a
936 /// suspend-containing `repeat(N)` loop's own entry block -- `count_expr`
937 /// (`RepeatLoop::condition()`, per LRM the loop's iteration COUNT, not a
938 /// boolean) is evaluated exactly ONCE, run after `stmts` and before
939 /// `terminator` fires (mirrors [`Self::step_exprs`]'s own "run after
940 /// stmts, before terminator" placement), its result (clamped to
941 /// non-negative) becoming `counter`'s starting value. `None` for every
942 /// other block, including a suspend-free `repeat` (which stays the
943 /// pre-existing opaque leaf `stmt_shared::lower_repeat_loop` already
944 /// handles -- see [`RepeatCounterId`]'s own doc comment).
945 pub repeat_init: Option<(RepeatCounterId, RepeatCount<'d>)>,
946 /// R-run Task 8: `Some(counter)` iff this block is a suspend-containing
947 /// `repeat(N)` loop's body's own back-edge block -- decrements
948 /// `counter` by exactly one, run after `stmts`/`step_exprs` and before
949 /// `terminator` fires. `None` for every other block.
950 pub repeat_decrement: Option<RepeatCounterId>,
951 /// `true` iff this block is the one an `expect` statement's wait resumes
952 /// into (LRM 1800-2023 §16.17): its first statement is that `expect` again,
953 /// whose resume half (the pass/`else` action, selected by the outcome the
954 /// process was woken with) codegen lowers there.
955 pub expect_resume: bool,
956 pub terminator: Terminator<'d>,
957}
958
959/// PR-004, PR-008: the CFG built by [`build_cfg`] -- one LLVM function per
960/// process (Task S3c) will eventually `switch` on `resume_pc` with one case
961/// per segment derived from `blocks` below.
962#[derive(Debug, Clone)]
963pub struct ProcessCfg<'d> {
964 pub blocks: Vec<BasicBlock<'d>>,
965 pub entry: BlockId,
966 /// The process body's original root `Statement` (Task S3b deviation,
967 /// not in Task S3a's original field list): `process::liveness::
968 /// compute_liveness`'s declared-local discovery needs to walk the
969 /// *pre-flattening* statement tree looking for `Block` statements'
970 /// `block_symbol()` scopes -- `blocks`/`stmts` above only ever retain
971 /// the leaf statements `CfgBuilder::walk_seq` flattened into each
972 /// `BasicBlock`, which discards every intervening `begin`/`end` `Block`
973 /// statement node (and, with it, that block's `StatementBlockSymbol` --
974 /// the ONLY place `sv-lang`=0.1.9's pinned C API exposes a declared
975 /// local's `Symbol`; there is no per-`VariableDeclaration`-statement
976 /// symbol accessor at all, confirmed against the real binding). Keeping
977 /// the untouched root here, alongside the flattened `blocks`, is the
978 /// minimal way to make both queries possible without re-walking from
979 /// the caller.
980 pub body: Statement<'d>,
981 /// The formals and locals of every task inlined into this CFG
982 /// ([`ArgBinding`]) -- declared locals of the process for frame-layout
983 /// purposes, though no `Block` under `body` declares them.
984 pub inlined_locals: Vec<Symbol<'d>>,
985 /// The target of each intra-assignment-timed assignment's captured
986 /// right-hand side ([`ArgBinding::Capture`]), indexed by `temp`: its
987 /// type sizes the temporary's frame slot.
988 pub captures: Vec<Expression<'d>>,
989 /// For a fork branch: the variables of the forking activation it uses
990 /// (LRM 1800-2023 §9.3.2, §6.21) -- shared with it, not copied: their
991 /// frame slots, here and in the parent, hold a pointer to one cell.
992 pub captured: Vec<Symbol<'d>>,
993 /// `(formal, actual)`: a `ref` formal of an inlined task that every call
994 /// in this process binds to the same static variable or net (LRM
995 /// 1800-2023 §13.5.2). The formal names the actual's storage -- it has no
996 /// frame slot and no copy-in/copy-out -- so a wait on it (`@(posedge
997 /// clk)` with `ref logic clk`) sees the actual change.
998 pub ref_bindings: Vec<(Symbol<'d>, Symbol<'d>)>,
999 /// The name(s) a `disable <label>;` ANYWHERE (this process, a sibling
1000 /// branch, an ancestor fork, or a genuinely different process) may
1001 /// single this CFG's own live process out by (LRM 9.6.2): the
1002 /// enclosing `fork : NAME`'s name, if this CFG is one of its branches,
1003 /// and/or this CFG's own named outermost block (`peer_block(body)`),
1004 /// if it has one. Empty when nothing could ever name this process this
1005 /// way. `vj-codegen::process_lower`'s fork/join lowering copies this
1006 /// into each branch's own [`super::super::sched::SpawnSpec::
1007 /// disable_name`].
1008 pub disable_names: Vec<String>,
1009 /// Every name [`Self::disable_names`] of a fork branch this CFG's own
1010 /// walk resolved a `disable <label>;` against WITHOUT a same-process
1011 /// lexical match (not an enclosing [`BasicBlock`], not this CFG's own
1012 /// `fork_name`, not a procedural-assertion label): the kept `disable`
1013 /// statement's codegen (`vj-codegen::process_lower`) reads this to
1014 /// tell such a statement apart from the ones that resolve to the OLDER,
1015 /// same-fork-only `sv_rt_disable_fork_block` path -- see
1016 /// [`CfgBuilder::fork_branch_names`]'s own doc comment for how it
1017 /// accumulates (parent-to-branch, branch-to-branch, and forward past
1018 /// the fork statement that spawned them).
1019 pub fork_branch_names: Vec<String>,
1020 /// `(resume, label, exit)`: while the process is suspended and will resume
1021 /// at block `resume`, it is inside the named block or task activation
1022 /// `label`, which a `disable label;` of another process ends by resuming
1023 /// the process at block `exit` instead (LRM 9.6.2). Only labels some
1024 /// `disable` of the design names ([`is_disabled_somewhere`]) are listed.
1025 /// A segment index is a valid resume point of the compiled kernel.
1026 pub disable_scopes: Vec<(BlockId, String, BlockId)>,
1027 /// The labels of the `disable` statements this CFG keeps as statements
1028 /// because they must also reach the processes other than this one that
1029 /// are inside the named block or task (`sv_rt_disable_named`), with
1030 /// whether the statement is the whole effect of the disable (an
1031 /// unresolved target: no jump follows, so codegen emits the VPI
1032 /// `cbDisable` probe itself).
1033 pub disable_broadcast: Vec<(String, bool)>,
1034 /// For a synthetic fork branch ([`CfgBuilder::walk_nba_event`]): the
1035 /// frame slots of the forking process copied BY VALUE into this
1036 /// branch's own at the spawn (LRM 1800-2023 §10.4.2: the right-hand
1037 /// side and the repeat count are evaluated when the assignment is
1038 /// reached, not when its event fires).
1039 pub inherited: Vec<InheritedSlot>,
1040 /// Per capture temporary (`captures`), the formal whose type sizes its
1041 /// frame slot instead of the captured expression's (a recursive task's
1042 /// call arguments, [`CfgBuilder::inline_recursive_task`]); entries past
1043 /// the end are `None`.
1044 pub capture_types: Vec<Option<Symbol<'d>>>,
1045 /// Whether this CFG is a recursive task's activation ([`Recursion`]).
1046 pub recursion: Recursion,
1047 /// Two fork branches carrying the same id were built from the same
1048 /// statement in the same inherited context ([`ForkBranchKey`]), so
1049 /// they are the same CFG and lower to the same kernel: the lowering
1050 /// compiles it once per process and points every spawn at that code
1051 /// (a UVM testbench otherwise cloned each branch once per path of
1052 /// nested forks leading to it, tens of GB of IR). `None` for a branch
1053 /// that is not shared.
1054 pub share_id: Option<u64>,
1055}
1056
1057/// A fork branch's part in a recursive task with timing controls (LRM
1058/// 1800-2023 §13.4, §13.4.1): every call is a `join` of one forked child
1059/// running the task body in a frame of its own.
1060#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1061pub enum Recursion {
1062 /// An ordinary CFG.
1063 None,
1064 /// The task's body: the branch the first call forks. Its kernel is
1065 /// compiled once and also serves every recursive call inside it.
1066 Root,
1067 /// A placeholder branch (no blocks of its own): a recursive call inside
1068 /// the enclosing [`Recursion::Root`], which spawns that root's kernel again.
1069 SelfSpawn,
1070 /// A placeholder branch: a call, inside one recursive activation, of the
1071 /// task of an enclosing activation (mutual recursion: `a` calls `b` calls
1072 /// `a`). It spawns that activation's kernel again; the number says how
1073 /// many [`Recursion::Root`] levels out it is (1 = the activation this one
1074 /// was forked from).
1075 AncestorSpawn(u32),
1076}
1077
1078/// Whether the process re-triggers from `entry` after finishing one pass
1079/// through its body (`always`/`always_ff`/`always_comb`/`always_latch`) or
1080/// runs exactly once (`initial`/`final`) -- PR-048's `Terminator::End{
1081/// loop_back}` distinction. Deliberately **not**
1082/// `vj-codegen::module::SliceProcKind`: this crate must never depend on
1083/// `vj-codegen` (see [`LoweringError`]'s doc comment for the same
1084/// reasoning); a caller that already has a `SliceProcKind` (module
1085/// discovery, Task S2/S7) maps it down to this coarser 2-value distinction
1086/// itself (`Initial` -> `OneShot`, `Always`/`AlwaysFF` -> `Looping`).
1087#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1088pub enum ProcessLoopKind {
1089 OneShot,
1090 Looping,
1091}
1092
1093/// PR-007: the single recursive-descent CFG builder entry point.
1094pub fn build_cfg<'d>(
1095 body: &Statement<'d>,
1096 loop_kind: ProcessLoopKind,
1097) -> Result<ProcessCfg<'d>, LoweringError> {
1098 build_cfg_in(
1099 body,
1100 loop_kind,
1101 None,
1102 None,
1103 None,
1104 None,
1105 Inherited::default(),
1106 None,
1107 )
1108}
1109
1110/// The CFG of a one-shot process that runs the body of `task` once, for a
1111/// task exported to C (`export "DPI-C" task`, LRM 1800-2023 35.7) whose body
1112/// consumes time (or stages a nonblocking assignment): C cannot run it as a
1113/// function, so each call from C spawns a process of this CFG and the
1114/// importing task blocks until it finishes (35.5.3).
1115///
1116/// The formals are ordinary locals of the process's frame, with NO entry
1117/// binding: the spawner stores the C arguments into their frame slots before
1118/// the process starts and reads the `output`/`inout` ones back when it ends
1119/// (the process's frame is what a copy-in/copy-out call has in a caller).
1120/// Other automatic locals get their declaration initialisers as usual.
1121/// `static` formals are module state, not frame slots; they stay out of
1122/// `inlined_locals`, exactly as a call inlined into a process treats them.
1123///
1124/// Fails by name for a task that calls itself (a recursive activation needs
1125/// the fork-per-call machinery of [`Recursion::Root`], whose argument
1126/// capture an export cannot supply).
1127pub fn build_export_task_cfg<'d>(
1128 task: &Symbol<'d>,
1129 global_clock: Option<InferredClockEvent<'d>>,
1130) -> Result<ProcessCfg<'d>, LoweringError> {
1131 let body = proc_body(task).ok_or_else(|| {
1132 LoweringError::malformed(format!("exported task `{}` has no body", task.name()))
1133 })?;
1134 let mut b = CfgBuilder::new();
1135 b.global_clock = global_clock;
1136 let entry = b.alloc_block();
1137 // Calling itself from its own body would inline without end; the
1138 // recursion machinery needs a caller's actuals to type its captures.
1139 b.rec_outer.push(task.uid());
1140 for f in task.subroutine_arguments() {
1141 if !crate::netlist::is_static_procedural_var(&f) {
1142 b.inlined_locals.push(f);
1143 }
1144 }
1145 for local in task_locals(task) {
1146 if crate::netlist::is_static_procedural_var(&local) {
1147 continue;
1148 }
1149 if !is_automatic_block_var(&local) {
1150 b.blocks[entry.0 as usize]
1151 .bindings
1152 .push(ArgBinding::Init { var: local });
1153 }
1154 b.inlined_locals.push(local);
1155 }
1156 for v in crate::patterns::pattern_vars(body.as_sem_node()) {
1157 if !b.inlined_locals.iter().any(|l| l.uid() == v.uid()) {
1158 b.inlined_locals.push(v);
1159 }
1160 }
1161 let after = b.alloc_block();
1162 b.target_depth.insert(after, 0);
1163 b.return_targets.push(after);
1164 let exit = b.walk_stmt(body, entry)?;
1165 b.close(exit, Terminator::Fallthrough(after));
1166 b.close(after, Terminator::End { loop_back: None });
1167 let inlined_locals = std::mem::take(&mut b.inlined_locals);
1168 let captures = std::mem::take(&mut b.captures);
1169 let capture_types = std::mem::take(&mut b.capture_types);
1170 let ref_bindings = b.take_ref_bindings();
1171 let disable_scopes = b.take_disable_scopes();
1172 let disable_broadcast = std::mem::take(&mut b.disable_broadcast);
1173 let fork_branch_names = std::mem::take(&mut b.fork_branch_names);
1174 Ok(ProcessCfg {
1175 blocks: b.finish(),
1176 entry,
1177 body,
1178 inlined_locals,
1179 captures,
1180 captured: Vec::new(),
1181 ref_bindings,
1182 disable_names: Vec::new(),
1183 fork_branch_names,
1184 disable_scopes,
1185 disable_broadcast,
1186 inherited: Vec::new(),
1187 capture_types,
1188 recursion: Recursion::None,
1189 share_id: None,
1190 })
1191}
1192
1193/// The one-shot process a task called from a sequence match item runs as
1194/// (LRM 1800-2023 16.11: "the subroutine call is scheduled in the Reactive
1195/// region ... as a thread of its own"). Its body is the call `call` (a user
1196/// task that can suspend, [`subroutine_can_suspend`]) inlined the way a call
1197/// statement of an ordinary process is: the formals are bound from the actual
1198/// expressions by [`ArgBinding::In`] in the entry block, so the process reads
1199/// its by-value actuals when it starts (the assertion kernel lowers each as the
1200/// value the engine sampled at the end point, `sva::proc::call_arg_key`) and
1201/// every call is its own activation with its own frame.
1202///
1203/// Fails by name for a task that calls itself (a recursive activation needs the
1204/// fork-per-call machinery of [`Recursion::Root`], whose argument capture a
1205/// spawned call cannot supply).
1206pub fn build_match_item_task_cfg<'d>(
1207 call: Expression<'d>,
1208) -> Result<ProcessCfg<'d>, LoweringError> {
1209 let task = call.call_subroutine().ok_or_else(|| {
1210 LoweringError::malformed("a match-item call of a task has no subroutine symbol")
1211 })?;
1212 let body = proc_body(&task)
1213 .ok_or_else(|| LoweringError::malformed(format!("task `{}` has no body", task.name())))?;
1214 let mut b = CfgBuilder::new();
1215 let entry = b.alloc_block();
1216 let exit = b.inline_task_call(call, task, entry)?;
1217 b.close(exit, Terminator::End { loop_back: None });
1218 let inlined_locals = std::mem::take(&mut b.inlined_locals);
1219 let captures = std::mem::take(&mut b.captures);
1220 let capture_types = std::mem::take(&mut b.capture_types);
1221 let ref_bindings = b.take_ref_bindings();
1222 let disable_scopes = b.take_disable_scopes();
1223 let disable_broadcast = std::mem::take(&mut b.disable_broadcast);
1224 let fork_branch_names = std::mem::take(&mut b.fork_branch_names);
1225 Ok(ProcessCfg {
1226 blocks: b.finish(),
1227 entry,
1228 body,
1229 inlined_locals,
1230 captures,
1231 captured: Vec::new(),
1232 ref_bindings,
1233 disable_names: Vec::new(),
1234 fork_branch_names,
1235 disable_scopes,
1236 disable_broadcast,
1237 inherited: Vec::new(),
1238 capture_types,
1239 recursion: Recursion::None,
1240 share_id: None,
1241 })
1242}
1243
1244/// The clocking event a checker's `$inferred_clock` (LRM 17.4) stands for: the
1245/// signal and edge of the default clocking of the scope that instantiates the
1246/// checker (14.12), which a checker procedure's `@clock` waits on when `clock`
1247/// kept its default. The third member is the event's `iff` condition
1248/// (`default clocking @(posedge clk iff en);`, 9.4.2.3), which gates every wait
1249/// on it; `None` for `@$global_clock`.
1250pub type InferredClockEvent<'d> = (Expression<'d>, EdgeKind, Option<Expression<'d>>);
1251
1252/// Whether `signal` (the expression of a signal event control) is a call of
1253/// `$global_clock` (14.14).
1254pub fn is_global_clock_call(signal: &Expression<'_>) -> bool {
1255 signal.kind() == ExpressionKind::Call
1256 && signal.is_system_call() == Some(true)
1257 && signal
1258 .system_subroutine()
1259 .is_some_and(|s| s.known_name_id() == sv_lang::KnownSystemName::GlobalClock)
1260}
1261
1262/// Whether `signal` (the expression of a signal event control) is a call of
1263/// `$inferred_clock`: a checker's `event clock = $inferred_clock` formal, used as
1264/// `@clock` by a procedure of an instance that did not bind it (17.4).
1265pub fn is_inferred_clock_call(signal: &Expression<'_>) -> bool {
1266 signal.kind() == ExpressionKind::Call
1267 && signal.is_system_call() == Some(true)
1268 && signal
1269 .system_subroutine()
1270 .is_some_and(|s| s.known_name_id() == sv_lang::KnownSystemName::InferredClock)
1271}
1272
1273/// The clocking block of a drive target rooted in a clock variable
1274/// (`cb.q`, `cb.q[3:0]`; LRM 1800-2023 14.16), else `None`.
1275fn drive_clocking_of<'d>(mut target: Expression<'d>) -> Option<Symbol<'d>> {
1276 while matches!(
1277 target.kind(),
1278 ExpressionKind::ElementSelect | ExpressionKind::RangeSelect
1279 ) {
1280 target = target.select_value()?;
1281 }
1282 let var = target.referenced_symbol()?;
1283 if var.kind() != SymbolKind::ClockVar {
1284 return None;
1285 }
1286 var.parent()
1287 .filter(|p| p.kind() == SymbolKind::ClockingBlock)
1288}
1289
1290/// The clock variable of a drive target rooted in a virtual interface member
1291/// access (`v.sb.c`, `v.sb.c[3:0]`), else `None`.
1292fn drive_via_of<'d>(mut target: Expression<'d>) -> Option<Expression<'d>> {
1293 while matches!(
1294 target.kind(),
1295 ExpressionKind::ElementSelect | ExpressionKind::RangeSelect
1296 ) {
1297 target = target.select_value()?;
1298 }
1299 (crate::netlist::vif::is_member_access(&target)).then_some(target)
1300}
1301
1302/// [`build_cfg`] for a process of an instance whose default clocking block
1303/// is `default_clocking` (what `##N` counts).
1304pub fn build_cfg_clocked<'d>(
1305 body: &Statement<'d>,
1306 loop_kind: ProcessLoopKind,
1307 default_clocking: Option<Symbol<'d>>,
1308) -> Result<ProcessCfg<'d>, LoweringError> {
1309 build_cfg_scoped(body, loop_kind, default_clocking, &[], None)
1310}
1311
1312/// [`build_cfg_clocked`] for a process that shares its instance with the
1313/// processes whose outermost named blocks are `peers`: a `disable` of one of
1314/// them is accepted when it cannot be inside the block (see [`PeerBlock`]).
1315/// `inferred_clock` is the event a checker procedure's `@clock` waits on when its
1316/// `clock` formal kept the default `$inferred_clock` (17.4); `None` for a process
1317/// that is not a checker's or whose instantiation context infers no clock.
1318pub fn build_cfg_scoped<'d>(
1319 body: &Statement<'d>,
1320 loop_kind: ProcessLoopKind,
1321 default_clocking: Option<Symbol<'d>>,
1322 peers: &[PeerBlock],
1323 inferred_clock: Option<InferredClockEvent<'d>>,
1324) -> Result<ProcessCfg<'d>, LoweringError> {
1325 build_cfg_in(
1326 body,
1327 loop_kind,
1328 None,
1329 default_clocking,
1330 Some(peers),
1331 inferred_clock,
1332 Inherited::default(),
1333 None,
1334 )
1335}
1336
1337/// [`build_cfg_scoped`] that can dispatch a virtual class task that suspends
1338/// over the class templates of `virtual_tasks`.
1339pub fn build_cfg_scoped_with_virtuals<'d>(
1340 body: &Statement<'d>,
1341 loop_kind: ProcessLoopKind,
1342 default_clocking: Option<Symbol<'d>>,
1343 peers: &[PeerBlock],
1344 inferred_clock: Option<InferredClockEvent<'d>>,
1345 global_clock: Option<InferredClockEvent<'d>>,
1346 virtual_tasks: Option<std::rc::Rc<VirtualTasks<'d>>>,
1347) -> Result<ProcessCfg<'d>, LoweringError> {
1348 build_cfg_in(
1349 body,
1350 loop_kind,
1351 None,
1352 default_clocking,
1353 Some(peers),
1354 inferred_clock,
1355 Inherited {
1356 global_clock,
1357 ..Inherited::default()
1358 },
1359 virtual_tasks,
1360 )
1361}
1362
1363/// The outermost named block of another process of the instance, which a
1364/// `disable` may target (LRM 9.6.2, 16.14.6.4).
1365#[derive(Clone, Debug, PartialEq, Eq)]
1366pub struct PeerBlock {
1367 pub name: String,
1368 /// The block contains a suspend point, so its process may be inside it. A
1369 /// `disable` then has to terminate the process's activity (9.6.2), which
1370 /// the scheduler cannot do; when the block cannot suspend, the process is
1371 /// never inside it while another runs, and the disable's only effect is the
1372 /// flush of the process's pending procedural assertion queue.
1373 pub can_suspend: bool,
1374 /// The block is the outermost block of a fork branch (its own process,
1375 /// tagged by name): any number of those may share a name, and `disable
1376 /// <name>;` ends every one (LRM 9.6.2).
1377 pub fork_branch: bool,
1378}
1379
1380/// The outermost scope of a process body (LRM 16.14.6.2): the block statement
1381/// the body is, looking through the leading timing controls of an `always`
1382/// (`always @(a or b) begin : b2 ... end`). `None` when the body is not a block.
1383pub fn outermost_block<'d>(body: &Statement<'d>) -> Option<Statement<'d>> {
1384 let mut cur = *body;
1385 loop {
1386 // Not a `match` over the kind: every other statement kind means "the body
1387 // is not a block", which is an answer, not an unimplemented case.
1388 let kind = cur.kind();
1389 if kind == StatementKind::Block {
1390 return Some(cur);
1391 }
1392 if kind == StatementKind::Timed {
1393 cur = cur.body()?;
1394 } else if kind == StatementKind::List {
1395 let list = live_statements(&cur);
1396 let [only] = list.as_slice() else {
1397 return None;
1398 };
1399 cur = *only;
1400 } else {
1401 return None;
1402 }
1403 }
1404}
1405
1406/// The named outermost block of the process body `body`, as another process
1407/// sees it ([`PeerBlock`]); `None` for an unnamed one.
1408pub fn peer_block<'d>(body: &Statement<'d>) -> Option<PeerBlock> {
1409 let block = outermost_block(body)?;
1410 let name = block_name(&block);
1411 if name.is_empty() {
1412 return None;
1413 }
1414 Some(PeerBlock {
1415 name,
1416 can_suspend: contains_suspend_trigger_in(
1417 block.as_sem_node(),
1418 &mut std::collections::HashSet::new(),
1419 true,
1420 false,
1421 ),
1422 fork_branch: false,
1423 })
1424}
1425
1426/// The hierarchical path of every named block (sequential or fork) in the
1427/// process body `body`, dotted from its outermost named block down
1428/// (`a`, `a.b`, `a.b.c`, `a.f.br1`): what a `disable s.a.b.c.inner;` from
1429/// another instance names, however deep (LRM 9.6.2, 23.6).
1430pub fn named_block_paths(body: &Statement<'_>) -> Vec<String> {
1431 let mut out = Vec::new();
1432 let mut stack: Vec<(SemNode<'_>, String)> = vec![(body.as_sem_node(), String::new())];
1433 while let Some((n, prefix)) = stack.pop() {
1434 let mut here = prefix.clone();
1435 if let Some(s) = n.as_statement()
1436 && s.kind() == StatementKind::Block
1437 {
1438 let name = block_name(&s);
1439 if !name.is_empty() {
1440 here = if prefix.is_empty() {
1441 name
1442 } else {
1443 format!("{prefix}.{name}")
1444 };
1445 out.push(here.clone());
1446 }
1447 }
1448 stack.extend(live_children(&n).into_iter().map(|c| (c, here.clone())));
1449 }
1450 out
1451}
1452
1453/// The named sequential blocks nested in the process body `body` below its
1454/// outermost scope, as another process sees them ([`PeerBlock`]): a `disable`
1455/// of one of them from another process ends the activity inside it (LRM
1456/// 9.6.2). A fork's branches are processes of their own and are not entered.
1457pub fn nested_peer_blocks<'d>(body: &Statement<'d>) -> Vec<PeerBlock> {
1458 let outermost = outermost_block(body).map(|b| b.source_range().start.offset);
1459 let mut out = Vec::new();
1460 let mut stack = vec![body.as_sem_node()];
1461 while let Some(n) = stack.pop() {
1462 if let Some(s) = n.as_statement()
1463 && s.kind() == StatementKind::Block
1464 {
1465 match s.block_kind() {
1466 Some(StatementBlockKind::Sequential) => {
1467 let name = block_name(&s);
1468 if !name.is_empty() && outermost != Some(s.source_range().start.offset) {
1469 out.push(PeerBlock {
1470 name,
1471 can_suspend: contains_suspend_trigger_in(
1472 n,
1473 &mut std::collections::HashSet::new(),
1474 true,
1475 false,
1476 ),
1477 fork_branch: false,
1478 });
1479 }
1480 }
1481 Some(
1482 StatementBlockKind::JoinAll
1483 | StatementBlockKind::JoinAny
1484 | StatementBlockKind::JoinNone,
1485 ) => {
1486 fork_branch_peers(n, &mut out);
1487 continue;
1488 }
1489 None => {}
1490 }
1491 }
1492 stack.extend(live_children(&n));
1493 }
1494 out
1495}
1496
1497/// The named outermost blocks of the branches of the fork `fork` and of every
1498/// fork nested in those branches: each branch is a process of its own tagged
1499/// with its block's name ([`ProcessCfg::disable_names`]), which `disable
1500/// <name>;` of another process ends (LRM 9.6.2). A branch's process may be
1501/// anywhere, so these always `can_suspend`. Named blocks nested deeper in a
1502/// branch are listed as ordinary nested peers.
1503fn fork_branch_peers(fork: SemNode<'_>, out: &mut Vec<PeerBlock>) {
1504 let mut branches = fork.children();
1505 if let [only] = branches.as_slice()
1506 && only
1507 .as_statement()
1508 .is_some_and(|s| s.kind() == StatementKind::List)
1509 {
1510 branches = only.children();
1511 }
1512 for b in branches {
1513 let Some(s) = b.as_statement() else { continue };
1514 if let Some(mut p) = peer_block(&s) {
1515 p.can_suspend = true;
1516 p.fork_branch = true;
1517 out.push(p);
1518 }
1519 // Blocks nested in the branch (and in forks nested in it) are blocks
1520 // of the branch's process: its kernel carries their disable scopes
1521 // (`ProcessCfg::disable_scopes`, `ForkKernel::disable_scopes`).
1522 out.extend(nested_peer_blocks(&s));
1523 }
1524}
1525
1526impl ProcessCfg<'_> {
1527 /// The labels of the disable scopes of every fork branch under this CFG
1528 /// (nested forks included): the blocks of the branch kernels a
1529 /// `disable` of another process can end (LRM 9.6.2).
1530 pub fn branch_disable_labels(&self) -> Vec<String> {
1531 let mut out = Vec::new();
1532 for b in &self.blocks {
1533 if let Terminator::ForkJoin { branches, .. } = &b.terminator {
1534 for br in branches {
1535 out.extend(br.disable_scopes.iter().map(|(_, l, _)| l.clone()));
1536 out.extend(br.branch_disable_labels());
1537 }
1538 }
1539 }
1540 out
1541 }
1542
1543 /// The `disable` broadcast targets of every fork branch under this CFG
1544 /// (nested forks included), as [`ProcessCfg::disable_broadcast`]: a
1545 /// `disable u1.blk;` inside a branch must resolve as one in the process
1546 /// itself does, or be refused (LRM 9.6.2).
1547 pub fn branch_disable_broadcast(&self) -> Vec<(String, bool)> {
1548 let mut out = Vec::new();
1549 for b in &self.blocks {
1550 if let Terminator::ForkJoin { branches, .. } = &b.terminator {
1551 for br in branches {
1552 out.extend(br.disable_broadcast.iter().cloned());
1553 out.extend(br.branch_disable_broadcast());
1554 }
1555 }
1556 }
1557 out
1558 }
1559}
1560
1561/// The label of every procedural concurrent assertion (not an `expect`, which
1562/// blocks) in the subtree of `body`, with the assertion statement: a labeled
1563/// statement is a block of that name holding it, which `disable <label>;`
1564/// names (16.14.6.4).
1565pub fn assertion_labels<'d>(body: &Statement<'d>) -> Vec<(String, Statement<'d>)> {
1566 let mut out = Vec::new();
1567 let mut stack = vec![body.as_sem_node()];
1568 while let Some(n) = stack.pop() {
1569 if let Some(s) = n.as_statement()
1570 && s.kind() == StatementKind::Block
1571 && let name = block_name(&s)
1572 && !name.is_empty()
1573 && let [only] = s.statements().as_slice()
1574 && only.kind() == StatementKind::ConcurrentAssertion
1575 && !is_expect(only)
1576 {
1577 out.push((name, *only));
1578 }
1579 stack.extend(live_children(&n));
1580 }
1581 out
1582}
1583
1584/// The label of every deferred immediate assertion (`L: assert #0 (c) ...;`,
1585/// `assert final`) in the subtree of `body`, with the assertion statement: a
1586/// labeled statement is a block of that name holding it, which `disable L;`
1587/// names (LRM 16.4.4: cancels that assertion's pending reports).
1588pub fn deferred_assertion_labels<'d>(body: &Statement<'d>) -> Vec<(String, Statement<'d>)> {
1589 let mut out = Vec::new();
1590 let mut stack = vec![body.as_sem_node()];
1591 while let Some(n) = stack.pop() {
1592 if let Some(s) = n.as_statement()
1593 && s.kind() == StatementKind::Block
1594 && let name = block_name(&s)
1595 && !name.is_empty()
1596 && let [only] = s.statements().as_slice()
1597 && only.kind() == StatementKind::ImmediateAssertion
1598 && only.is_deferred_assertion() == Some(true)
1599 {
1600 out.push((name, *only));
1601 }
1602 stack.extend(live_children(&n));
1603 }
1604 out
1605}
1606
1607/// The label of every plain (non-deferred) immediate assertion in the subtree
1608/// of `body`: a labeled statement is a block of that name holding it. A
1609/// `disable L;` outside that block (LRM 9.6.2) finds the block already
1610/// finished -- an immediate assertion's own block has no suspend point the
1611/// disable could catch from the same process.
1612pub fn immediate_assertion_labels(body: &Statement<'_>) -> Vec<String> {
1613 let mut out = Vec::new();
1614 let mut stack = vec![body.as_sem_node()];
1615 while let Some(n) = stack.pop() {
1616 if let Some(s) = n.as_statement()
1617 && s.kind() == StatementKind::Block
1618 && let name = block_name(&s)
1619 && !name.is_empty()
1620 && let [only] = s.statements().as_slice()
1621 && only.kind() == StatementKind::ImmediateAssertion
1622 && only.is_deferred_assertion() != Some(true)
1623 {
1624 out.push(name);
1625 }
1626 stack.extend(live_children(&n));
1627 }
1628 out
1629}
1630
1631/// Whether `stmt` is a labeled immediate assertion (`L: assert (c) begin
1632/// ... disable L; ... end`, a block named `L` holding only the assertion)
1633/// whose only `disable` targets `L` itself and which contains no other
1634/// suspend point. Disabling the label ends the assertion statement (LRM
1635/// 9.6.2: a labeled statement is a named scope), so the whole block is one
1636/// straight-line leaf for the CFG and codegen leaves it through an exit
1637/// block (`process_lower`'s `List | Block` arm).
1638pub fn self_disabling_assertion_block(stmt: &Statement<'_>) -> bool {
1639 if stmt.kind() != StatementKind::Block {
1640 return false;
1641 }
1642 let name = block_name(stmt);
1643 if name.is_empty() {
1644 return false;
1645 }
1646 let children = stmt.statements();
1647 let [only] = children.as_slice() else {
1648 return false;
1649 };
1650 if only.kind() != StatementKind::ImmediateAssertion {
1651 return false;
1652 }
1653 let mut disables = 0usize;
1654 let mut stack = vec![only.as_sem_node()];
1655 while let Some(n) = stack.pop() {
1656 if let Some(s) = n.as_statement()
1657 && s.kind() == StatementKind::Disable
1658 {
1659 match disable_label(&s) {
1660 Ok(l) if l == name => disables += 1,
1661 _ => return false,
1662 }
1663 }
1664 stack.extend(live_children(&n));
1665 }
1666 disables > 0
1667 && !contains_suspend_trigger_in(
1668 only.as_sem_node(),
1669 &mut std::collections::HashSet::new(),
1670 true,
1671 false,
1672 )
1673}
1674
1675/// The CFG of the plan that hosts the `expect` statement `stmt` for the
1676/// assertion engine (LRM 1800-2023 §16.17): `stmt` is an ordinary leaf of it,
1677/// not a wait -- the process that blocks on the `expect` is another plan, built
1678/// by [`build_cfg_clocked`].
1679pub fn build_cfg_hosted_assertion<'d>(
1680 stmt: &Statement<'d>,
1681 default_clocking: Option<Symbol<'d>>,
1682) -> Result<ProcessCfg<'d>, LoweringError> {
1683 let mut builder = CfgBuilder::new();
1684 builder.default_clocking = default_clocking;
1685 builder.expect_as_leaf = true;
1686 let entry = builder.alloc_block();
1687 let exit = builder.walk_stmt(*stmt, entry)?;
1688 builder.close(exit, Terminator::End { loop_back: None });
1689 Ok(ProcessCfg {
1690 blocks: builder.finish(),
1691 entry,
1692 body: *stmt,
1693 inlined_locals: Vec::new(),
1694 captures: Vec::new(),
1695 captured: Vec::new(),
1696 ref_bindings: Vec::new(),
1697 disable_names: Vec::new(),
1698 fork_branch_names: Vec::new(),
1699 disable_scopes: Vec::new(),
1700 disable_broadcast: Vec::new(),
1701 inherited: Vec::new(),
1702 capture_types: Vec::new(),
1703 recursion: Recursion::None,
1704 share_id: None,
1705 })
1706}
1707
1708/// Whether a call was written `super.m(...)` (LRM 8.15: not virtual).
1709pub type SuperDetector = fn(Expression<'_>) -> bool;
1710
1711thread_local! {
1712 /// The [`SuperDetector`]: recovering `super.` needs the design's syntax,
1713 /// which codegen holds (`vj_codegen::classes::call_is_explicit_super`);
1714 /// the run installs it.
1715 static EXPLICIT_SUPER: std::cell::Cell<Option<SuperDetector>> =
1716 const { std::cell::Cell::new(None) };
1717}
1718
1719/// Installs (or, with `None`, removes) the `super.`-call detector
1720/// [`CfgBuilder::inline_task_call`] uses for virtual class tasks.
1721pub fn set_explicit_super_detector(f: Option<SuperDetector>) {
1722 EXPLICIT_SUPER.with(|c| c.set(f));
1723}
1724
1725fn is_explicit_super(call: Expression<'_>) -> bool {
1726 EXPLICIT_SUPER.with(|c| c.get().is_some_and(|f| f(call)))
1727}
1728
1729/// The design's class templates a dynamic type can be (every `new()`-reachable
1730/// class and its ancestors, with their dense template ids), for dispatching a
1731/// virtual class task that can suspend: such a task is inlined, so a call
1732/// through a base handle becomes a switch over the overrides the possible
1733/// dynamic classes resolve it to.
1734pub struct VirtualTasks<'d> {
1735 pub classes: Vec<(u32, sv_lang::Type<'d>)>,
1736 /// Per entry of `classes`, its base chain (root first, itself last) and
1737 /// the chain's symbol keys, filled on first use: `implementations` runs
1738 /// once per virtual method per fixed-point round, and rebuilding the
1739 /// chain and every ancestor's hierarchical path each time dominated the
1740 /// compile of a deep class library (`import uvm_pkg::*`).
1741 chains: std::cell::OnceCell<Vec<ClassChain<'d>>>,
1742 /// Per task, whether its body reaches itself ([`task_reaches`] with the
1743 /// task as target): decided once per task, not per call site, because
1744 /// each query walks every task reachable through virtual dispatch (a
1745 /// UVM testbench compiled for over an hour re-walking `uvm_pkg`).
1746 self_reach: std::cell::RefCell<std::collections::HashMap<crate::symid::SymbolId, bool>>,
1747}
1748
1749/// One class's base chain and the keys of its members ([`VirtualTasks`]).
1750struct ClassChain<'d> {
1751 chain: Vec<sv_lang::Type<'d>>,
1752 keys: std::collections::HashSet<crate::symid::SymbolId>,
1753}
1754
1755thread_local! {
1756 /// Every virtual method one of whose implementations (in the classes a
1757 /// dynamic type can be) can suspend ([`set_suspending_virtuals`]).
1758 static SUSPENDING_VIRTUALS: std::cell::RefCell<std::collections::HashSet<crate::symid::SymbolId>> =
1759 std::cell::RefCell::new(std::collections::HashSet::new());
1760}
1761
1762fn is_suspending_virtual(sub: &Symbol<'_>) -> bool {
1763 SUSPENDING_VIRTUALS.with(|s| s.borrow().contains(&sub.uid()))
1764}
1765
1766/// Records which virtual methods can suspend through an override (LRM 8.20:
1767/// a call runs the dynamic class's implementation, so it suspends when any
1768/// implementation can): such a call is inlined as a dispatch over the
1769/// overrides. To a fixed point, since an implementation may suspend only
1770/// through another virtual call. Clears the previous design's record.
1771pub fn set_suspending_virtuals(vt: &VirtualTasks<'_>) {
1772 SUSPENDING_VIRTUALS.with(|s| s.borrow_mut().clear());
1773 let mut methods: Vec<(crate::symid::SymbolId, Symbol<'_>)> = Vec::new();
1774 for (_, ty) in &vt.classes {
1775 for m in ty.as_symbol().members() {
1776 let m = if m.kind() == SymbolKind::MethodPrototype {
1777 match m.method_prototype_subroutine() {
1778 Some(real) => real,
1779 None => continue,
1780 }
1781 } else {
1782 m
1783 };
1784 if m.kind() == SymbolKind::Subroutine
1785 && (m.subroutine_this_var().is_some()
1786 || m.subroutine_flags().contains(sv_lang::MethodFlags::PURE))
1787 && (m.subroutine_flags().contains(sv_lang::MethodFlags::VIRTUAL)
1788 || m.subroutine_override().is_some())
1789 && !methods.iter().any(|(_, x)| *x == m)
1790 {
1791 methods.push((m.uid(), m));
1792 }
1793 }
1794 }
1795 loop {
1796 let mut changed = false;
1797 for (uid, m) in &methods {
1798 if SUSPENDING_VIRTUALS.with(|s| s.borrow().contains(uid)) {
1799 continue;
1800 }
1801 if vt
1802 .implementations(m)
1803 .iter()
1804 .any(|(_, imp)| subroutine_can_suspend(imp))
1805 {
1806 SUSPENDING_VIRTUALS.with(|s| s.borrow_mut().insert(uid.clone()));
1807 changed = true;
1808 }
1809 }
1810 if !changed {
1811 break;
1812 }
1813 }
1814}
1815
1816impl<'d> VirtualTasks<'d> {
1817 /// The possible dynamic classes (template id, class type).
1818 pub fn new(classes: Vec<(u32, sv_lang::Type<'d>)>) -> Self {
1819 VirtualTasks {
1820 classes,
1821 chains: std::cell::OnceCell::new(),
1822 self_reach: std::cell::RefCell::new(std::collections::HashMap::new()),
1823 }
1824 }
1825
1826 /// The implementations of `method` the dynamic classes derived from (or
1827 /// equal to) its declaring class run, each with the template ids that
1828 /// run it: per class, the most-derived declaration of the name with a
1829 /// body.
1830 pub fn implementations(&self, method: &Symbol<'d>) -> Vec<(Vec<u32>, Symbol<'d>)> {
1831 let Some(decl) = method.parent() else {
1832 return Vec::new();
1833 };
1834 let name = method.name();
1835 let decl_key = decl.uid();
1836 let chains = self.chains.get_or_init(|| {
1837 self.classes
1838 .iter()
1839 .map(|(_, ty)| {
1840 let mut chain = crate::netlist::base_chain(*ty);
1841 chain.push(*ty);
1842 let keys = chain.iter().map(|c| c.as_symbol().uid()).collect();
1843 ClassChain { chain, keys }
1844 })
1845 .collect()
1846 });
1847 let mut out: Vec<(Vec<u32>, Symbol<'d>)> = Vec::new();
1848 for ((id, _), cc) in self.classes.iter().zip(chains) {
1849 if !cc.keys.contains(&decl_key) {
1850 continue;
1851 }
1852 let found = cc.chain.iter().rev().find_map(|c| {
1853 let s = c.as_symbol().find(name)?;
1854 let s = if s.kind() == SymbolKind::MethodPrototype {
1855 s.method_prototype_subroutine()?
1856 } else {
1857 s
1858 };
1859 (s.kind() == SymbolKind::Subroutine
1860 && s.body().is_some()
1861 && !s.subroutine_flags().contains(sv_lang::MethodFlags::PURE))
1862 .then_some(s)
1863 });
1864 let Some(imp) = found else { continue };
1865 match out.iter_mut().find(|(_, s)| *s == imp) {
1866 Some((ids, _)) => ids.push(*id),
1867 None => out.push((vec![*id], imp)),
1868 }
1869 }
1870 out
1871 }
1872}
1873
1874/// [`build_cfg`] for one branch of the fork block named `fork_name`, if
1875/// named: a `disable` of that name inside the branch ends the fork.
1876#[allow(clippy::too_many_arguments)]
1877fn build_cfg_in<'d>(
1878 body: &Statement<'d>,
1879 loop_kind: ProcessLoopKind,
1880 fork_name: Option<String>,
1881 default_clocking: Option<Symbol<'d>>,
1882 peers: Option<&[PeerBlock]>,
1883 checker_clock: Option<InferredClockEvent<'d>>,
1884 inherited: Inherited<'d>,
1885 virtual_tasks: Option<std::rc::Rc<VirtualTasks<'d>>>,
1886) -> Result<ProcessCfg<'d>, LoweringError> {
1887 // LRM 9.6.2: the name(s) a `disable` ANYWHERE may single this CFG's own
1888 // process out by -- computed before `fork_name` moves into `builder`.
1889 let mut disable_names: Vec<String> = Vec::new();
1890 disable_names.extend(fork_name.clone());
1891 disable_names.extend(peer_block(body).map(|p| p.name));
1892
1893 let mut builder = CfgBuilder::new();
1894 builder.this_stack.extend(inherited.this_var);
1895 builder.ref_stack = inherited.ref_aliases;
1896 builder.global_clock = inherited.global_clock;
1897 builder.detached_trigger = inherited.detached_trigger;
1898 builder.forced_extern = inherited.forced_extern;
1899 builder.rec_outer = inherited.rec_outer;
1900 if let Some(memo) = inherited.fork_memo {
1901 builder.fork_memo = memo;
1902 }
1903 if let Some(rec) = inherited.rec {
1904 // The same capture temporaries the activation reserves.
1905 for _ in 0..rec.groups {
1906 for &(a, f) in &rec.proto {
1907 builder.set_capture_type(a, f);
1908 }
1909 }
1910 builder.rec = Some(rec);
1911 }
1912 builder.rec_up = inherited.rec_up;
1913 builder.virtual_tasks = virtual_tasks;
1914 builder.fork_name = fork_name;
1915 builder.fork_branch_names = inherited.known_fork_labels;
1916 // LRM 9.6.2: a branch also answers to `disable` of any block enclosing
1917 // its fork (it is an activity enabled within that block), so a block that
1918 // ends with detached branches still running ends them. Only names some
1919 // `disable` of the design mentions are tagged.
1920 builder.disable_names_extra(&inherited.ancestor_blocks, &mut disable_names);
1921 builder.body_node = Some(body.as_sem_node());
1922 builder.ancestor_blocks = inherited.ancestor_blocks;
1923 builder.default_clocking = default_clocking;
1924 builder.checker_clock = checker_clock;
1925 if let Some(peers) = peers {
1926 // A fork branch is not the process's own scope: only a whole process
1927 // has an outermost scope, assertion labels and peers.
1928 builder.peers = peers.to_vec();
1929 builder.assertion_labels = assertion_labels(body)
1930 .into_iter()
1931 .map(|(name, _)| name)
1932 .collect();
1933 builder.deferred_labels = deferred_assertion_labels(body)
1934 .into_iter()
1935 .map(|(name, _)| name)
1936 .collect();
1937 builder.immediate_labels = immediate_assertion_labels(body);
1938 builder.outermost = peer_block(body).map(|p| p.name);
1939 }
1940 let entry = builder.alloc_block();
1941 let exit = builder.walk_stmt(*body, entry)?;
1942 let loop_back = match loop_kind {
1943 ProcessLoopKind::OneShot => None,
1944 ProcessLoopKind::Looping => Some(entry),
1945 };
1946 builder.close(exit, Terminator::End { loop_back });
1947 let ref_bindings = builder.take_ref_bindings();
1948 let inlined_locals = std::mem::take(&mut builder.inlined_locals);
1949 let captures = std::mem::take(&mut builder.captures);
1950 let capture_types = std::mem::take(&mut builder.capture_types);
1951 let fork_branch_names = std::mem::take(&mut builder.fork_branch_names);
1952 let disable_scopes = builder.take_disable_scopes();
1953 let disable_broadcast = std::mem::take(&mut builder.disable_broadcast);
1954 Ok(ProcessCfg {
1955 blocks: builder.finish(),
1956 entry,
1957 body: *body,
1958 inlined_locals,
1959 captures,
1960 captured: Vec::new(),
1961 ref_bindings,
1962 disable_names,
1963 fork_branch_names,
1964 disable_scopes,
1965 disable_broadcast,
1966 inherited: Vec::new(),
1967 capture_types,
1968 recursion: Recursion::None,
1969 share_id: None,
1970 })
1971}
1972
1973/// The recursive task whose activation a builder is walking.
1974#[derive(Clone)]
1975struct RecursiveTask<'d> {
1976 task: crate::symid::SymbolId,
1977 /// The [`ProcessCfg::share_id`] of this activation's Root (unique per
1978 /// distinct [`RootKey`]); part of the key of every Root nested in it.
1979 share: u64,
1980 /// The first call's actuals and the formals they shape (the capture
1981 /// temporaries' types, [`CfgBuilder::set_capture_type`]), so a fork
1982 /// branch inside the activation can reserve the same temporaries.
1983 proto: Vec<(Expression<'d>, Symbol<'d>)>,
1984 /// The number of formals: capture temporaries `0..n` hold the incoming
1985 /// arguments, `n..2n` the arguments of a recursive call.
1986 n: u32,
1987 /// The number of temporary groups of `n`: 2, or 4 when a formal is an
1988 /// `output`/`inout` (`2n..3n` the activation's results, `3n..4n` those
1989 /// of a recursive call).
1990 groups: u32,
1991 /// Per formal: whether it is an `output`/`inout`.
1992 outs: Vec<bool>,
1993 /// Per formal: the static storage an aliased `ref` formal names.
1994 refs: Vec<Option<Symbol<'d>>>,
1995 /// A class method: capture temporary 0 (formal 0) is its `this`.
1996 has_this: bool,
1997 body: Statement<'d>,
1998}
1999
2000/// Everything a fork branch's CFG is a function of besides its statement:
2001/// the key of [`CfgBuilder::fork_memo`]. A branch is walked by a fresh
2002/// builder seeded only from [`Inherited`], so equal keys give equal CFGs.
2003#[derive(PartialEq, Eq, Hash)]
2004struct ForkBranchKey {
2005 /// Address and kind of the branch statement's AST node.
2006 stmt: (usize, u16),
2007 fork_name: Option<String>,
2008 default_clocking: Option<crate::symid::SymbolId>,
2009 this_var: Option<crate::symid::SymbolId>,
2010 ref_aliases: Vec<(crate::symid::SymbolId, crate::symid::SymbolId)>,
2011 rec_outer: Vec<crate::symid::SymbolId>,
2012 labels: Vec<String>,
2013 ancestors: Vec<String>,
2014}
2015
2016/// Everything a [`Recursion::Root`] activation's CFG is a function of: the
2017/// key of its entry in [`CfgBuilder::fork_memo`]. A Root is walked by a fresh
2018/// builder seeded from exactly these fields (the call site's actuals only
2019/// supply per-site capture bindings; the capture temporaries are typed by
2020/// the formals), so equal keys give equal CFGs. `chain` is the
2021/// [`RecursiveTask::share`] ids of the enclosing activations, which pins
2022/// each one's CFG (and so its entry block and frame layout, which an
2023/// [`Recursion::AncestorSpawn`] inside this Root copies from).
2024#[derive(PartialEq, Eq, Hash)]
2025struct RootKey {
2026 task: crate::symid::SymbolId,
2027 this_var: Option<crate::symid::SymbolId>,
2028 default_clocking: Option<crate::symid::SymbolId>,
2029 rec_outer: Vec<crate::symid::SymbolId>,
2030 chain: Vec<u64>,
2031 ref_aliases: Vec<(crate::symid::SymbolId, crate::symid::SymbolId)>,
2032 labels: Vec<String>,
2033 ancestors: Vec<String>,
2034}
2035
2036/// A [`CfgBuilder::fork_memo`] key: a fork branch or a recursive-task Root.
2037#[derive(PartialEq, Eq, Hash)]
2038enum MemoKey {
2039 Fork(ForkBranchKey),
2040 Root(RootKey),
2041}
2042
2043/// The fork branches and recursive-task Roots of one process already built,
2044/// by [`MemoKey`].
2045type ForkMemo<'d> =
2046 std::rc::Rc<std::cell::RefCell<std::collections::HashMap<MemoKey, ProcessCfg<'d>>>>;
2047
2048/// Source of [`ProcessCfg::share_id`]s (unique across the program).
2049static NEXT_SHARE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
2050
2051/// The AST node address of a statement handle (a handle is a copy of the
2052/// FFI node pointer; two handles to one node compare equal here).
2053fn stmt_addr(s: &Statement<'_>) -> usize {
2054 // SAFETY: `Statement` wraps one `repr(C)` `slang_ast` whose first field
2055 // is the node pointer; reading that word reads the pointer.
2056 unsafe { *std::ptr::from_ref(s).cast::<usize>() }
2057}
2058
2059/// What a fork branch inherits from the inlined task it is forked in: its
2060/// `this` and its aliased `ref` formals.
2061#[derive(Default)]
2062struct Inherited<'d> {
2063 this_var: Option<Symbol<'d>>,
2064 ref_aliases: Vec<(Symbol<'d>, Symbol<'d>)>,
2065 /// [`CfgBuilder::global_clock`].
2066 global_clock: Option<InferredClockEvent<'d>>,
2067 /// [`CfgBuilder::detached_trigger`].
2068 detached_trigger: bool,
2069 /// [`CfgBuilder::forced_extern`].
2070 forced_extern: Option<Symbol<'d>>,
2071 /// [`CfgBuilder::rec_outer`].
2072 rec_outer: Vec<crate::symid::SymbolId>,
2073 /// [`CfgBuilder::fork_memo`], shared by a whole process's builders.
2074 fork_memo: Option<ForkMemo<'d>>,
2075 /// [`CfgBuilder::rec`]: a fork branch inside a recursive activation can
2076 /// itself call the task (each such call spawns the activation again).
2077 rec: Option<RecursiveTask<'d>>,
2078 /// [`CfgBuilder::rec_up`].
2079 rec_up: Vec<RecursiveTask<'d>>,
2080 /// [`CfgBuilder::fork_branch_names`]'s own starting point: every label
2081 /// already known reachable from here (the parent's own accumulated list
2082 /// at the fork site, plus this fork's sibling branches) before this
2083 /// CFG's own walk adds any it discovers deeper in its own body.
2084 known_fork_labels: Vec<String>,
2085 /// [`CfgBuilder::ancestor_blocks`]' starting point.
2086 ancestor_blocks: Vec<String>,
2087}
2088
2089/// A VPI frame probe of an inlined blocking task (37.43): its activation
2090/// begins (after its arguments and locals are set up) or ends.
2091#[derive(Clone, Debug)]
2092pub enum FrameProbe<'d> {
2093 Enter {
2094 task: Symbol<'d>,
2095 /// Its automatic variables: formals, then locals.
2096 vars: Vec<Symbol<'d>>,
2097 /// The `ref` formals the call bound by copy-in/copy-out (an actual
2098 /// that is not static storage) with their actuals, in formal order
2099 /// (37.15 detail 3). They are ref objs of the frame, not variables
2100 /// (so absent from `vars`).
2101 bound: Vec<(Symbol<'d>, Expression<'d>)>,
2102 },
2103 Exit {
2104 task: Symbol<'d>,
2105 vars: Vec<Symbol<'d>>,
2106 },
2107}
2108
2109struct PartialBlock<'d> {
2110 bindings: Vec<ArgBinding<'d>>,
2111 this_var: Option<Symbol<'d>>,
2112 ref_aliases: Vec<(Symbol<'d>, Symbol<'d>)>,
2113 stmts: Vec<Statement<'d>>,
2114 probes: Vec<(usize, Statement<'d>)>,
2115 disable_probes: Vec<(usize, Statement<'d>)>,
2116 frame_probes: Vec<(usize, FrameProbe<'d>)>,
2117 block_events: Vec<(usize, Symbol<'d>, bool)>,
2118 stmt_scopes: Vec<(usize, Symbol<'d>)>,
2119 step_exprs: Vec<Expression<'d>>,
2120 repeat_init: Option<(RepeatCounterId, RepeatCount<'d>)>,
2121 repeat_decrement: Option<RepeatCounterId>,
2122 expect_resume: bool,
2123 terminator: Option<Terminator<'d>>,
2124}
2125
2126struct CfgBuilder<'d> {
2127 blocks: Vec<PartialBlock<'d>>,
2128 /// Task 1: the single clock signal+edge `##N` (`CycleDelay`) is lowered
2129 /// against, inferred from the first `posedge`/`negedge` `SuspendPoint::
2130 /// Edge` this walk encounters (in practice, the enclosing `always_ff`'s
2131 /// own edge). `None` until seen; becomes permanently ambiguous
2132 /// (`Some(None)`) the moment a *second*, differently-referenced edge
2133 /// signal (or the SAME signal with a different edge -- Task 6 extends
2134 /// the ambiguity check to cover this too, since `CycleDelay` now carries
2135 /// a real `edge` field it must be unambiguous about) is seen, so a later
2136 /// `##N` inside a multi-clock or unclocked-context process is a named
2137 /// `LoweringError`, never a silent guess (`walk_timed`'s `CycleDelay`
2138 /// arm). Task 6: also threaded through as real `SuspendPoint::
2139 /// CycleDelay{clock, edge, ..}` payload -- previously computed
2140 /// then discarded once the ambiguity check passed.
2141 inferred_clock: Option<Option<(Expression<'d>, EdgeKind)>>,
2142 /// R-run Task 8: the next fresh [`RepeatCounterId`] this builder will
2143 /// mint -- dense, allocation-order-numbered, mirrors [`Self::blocks`]'s
2144 /// own `len()`-as-next-id discipline for [`BlockId`].
2145 next_repeat_counter: u32,
2146 /// Formals/locals of every task inlined so far (`ProcessCfg::
2147 /// inlined_locals`).
2148 inlined_locals: Vec<Symbol<'d>>,
2149 /// Every inlined call's `ref` formal with the symbol its actual names
2150 /// (`None` for an actual that is not a plain name): resolved into
2151 /// `ProcessCfg::ref_bindings` once the whole process is walked.
2152 ref_candidates: Vec<(Symbol<'d>, Option<Symbol<'d>>)>,
2153 /// Set while walking a block whose automatic variables were just
2154 /// bound on entry (so the block itself is walked normally).
2155 entering_block: bool,
2156 /// The call being inlined, whose type shapes the `this` capture of a
2157 /// recursive method call with an implicit receiver.
2158 call_anchor: Option<Expression<'d>>,
2159 /// The capture temporary the next [`Self::inline_task_call`] parks the
2160 /// called function's result in ([`ArgBinding::CallResult`]).
2161 pending_result_temp: Option<(u32, Expression<'d>)>,
2162 /// Where a `return;` inside the innermost task being inlined goes.
2163 return_targets: Vec<BlockId>,
2164 /// The result variable of each inlined function (`None` for a task or void
2165 /// function), parallel to the inlined entries of [`Self::return_targets`]:
2166 /// `return expr;` assigns it (LRM 1800-2023 13.4.1).
2167 ret_var_stack: Vec<Option<Symbol<'d>>>,
2168 /// The `this` of each class-method task being inlined, innermost last
2169 /// ([`BasicBlock::this_var`]).
2170 this_stack: Vec<Symbol<'d>>,
2171 /// The `ref` formals of the tasks being inlined that alias their call's
2172 /// storage ([`BasicBlock::ref_aliases`]), innermost last.
2173 ref_stack: Vec<(Symbol<'d>, Symbol<'d>)>,
2174 /// The class templates a virtual suspending task can dispatch over.
2175 virtual_tasks: Option<std::rc::Rc<VirtualTasks<'d>>>,
2176 /// The type source of each capture temporary ([`ProcessCfg::capture_types`]).
2177 capture_types: Vec<Option<Symbol<'d>>>,
2178 /// Tasks currently being inlined (recursion guard).
2179 inline_stack: Vec<crate::symid::SymbolId>,
2180 /// While building a recursive task's activation
2181 /// ([`Self::inline_recursive_task`]): the task, whose calls are forks.
2182 rec: Option<RecursiveTask<'d>>,
2183 /// The recursive activations enclosing [`Self::rec`]'s (outermost first):
2184 /// a call of one of their tasks is a spawn of that activation
2185 /// ([`Recursion::AncestorSpawn`]).
2186 rec_up: Vec<RecursiveTask<'d>>,
2187 /// The recursive tasks whose activation encloses this builder's CFG (a
2188 /// fork branch inside one cannot start another activation of it).
2189 rec_outer: Vec<crate::symid::SymbolId>,
2190 /// The innermost enclosing loop's `continue` and `break` targets.
2191 loop_targets: Vec<(BlockId, BlockId)>,
2192 /// The sequential blocks being walked that declare automatic variables
2193 /// holding class or collection handles, innermost last (LRM 6.21: such a
2194 /// variable ends with its block, and a handle is released then).
2195 owned_scopes: Vec<Vec<Symbol<'d>>>,
2196 /// For each jump target (`break`/`continue`/`return`/`disable` exit), how
2197 /// many [`Self::owned_scopes`] were open outside it: a jump to it
2198 /// releases the scopes above that depth ([`Self::jump`]).
2199 target_depth: std::collections::HashMap<BlockId, usize>,
2200 /// The enclosing named blocks a `disable` may leave: `(name, exit)`,
2201 /// innermost last.
2202 /// `(label, exit, frames)`: `frames` is how many inlined tasks were
2203 /// active (`frame_stack`) when the block was entered.
2204 disable_targets: Vec<(String, BlockId, usize)>,
2205 /// The body this builder walks, for the lazy [`Self::body_forks`] scan.
2206 body_node: Option<SemNode<'d>>,
2207 /// The names of the fork blocks this builder walked (a subset of
2208 /// [`Self::fork_branch_names`]): a `fl.b.x` path through one is an
2209 /// instance-path-style label, not a branch name.
2210 fork_block_names: Vec<String>,
2211 /// Whether the body (or a task it calls) holds a fork block: decided on
2212 /// the first `disable` that jumps out of a named block, since only then
2213 /// can detached branches need ending with it (LRM 9.6.2).
2214 body_forks: Option<bool>,
2215 /// The covergroup-event blocks and inlined tasks being walked, innermost
2216 /// last, each with `disable_targets.len()` when it was entered: a
2217 /// `disable` that jumps out of them ends them there (LRM 9.6.2, 19.3), so
2218 /// [`Self::left_block_events`] signals their `end` at the jump.
2219 event_stack: Vec<(Symbol<'d>, usize)>,
2220 /// The tasks being inlined, innermost last, with their automatic
2221 /// variables (for the VPI frame probes a `disable` jumping out of them
2222 /// owes).
2223 frame_stack: Vec<(Symbol<'d>, Vec<Symbol<'d>>)>,
2224 /// The named blocks and inlined task activations being walked whose name
2225 /// some `disable` of the design targets, `(label, exit)`, innermost last:
2226 /// every block allocated while one is open is inside it
2227 /// ([`Self::block_scopes`], [`ProcessCfg::disable_scopes`]).
2228 /// `frames` is how many inlined tasks were active ([`Self::frame_stack`])
2229 /// outside the scope.
2230 scope_chain: Vec<(String, BlockId, usize)>,
2231 /// The names of the named sequential blocks being walked, outermost first
2232 /// (the path `disable p.b.x;` names, LRM 9.6.2).
2233 name_path: Vec<String>,
2234 /// [`Self::scope_chain`] as it was when each block was allocated.
2235 block_scopes: Vec<Vec<(String, BlockId, usize)>>,
2236 /// [`Self::frame_stack`] as it was when each block was allocated (empty
2237 /// outside every scope).
2238 block_frames: Vec<Vec<(Symbol<'d>, Vec<Symbol<'d>>)>>,
2239 /// [`ProcessCfg::disable_broadcast`].
2240 disable_broadcast: Vec<(String, bool)>,
2241 /// When this CFG is one branch of a named fork block: that name, which
2242 /// a `disable` inside it may target (`sv_rt_disable_fork_block`).
2243 fork_name: Option<String>,
2244 /// Every fork-branch [`ProcessCfg::disable_names`] a `disable <label>;`
2245 /// written anywhere in THIS CFG's own remaining walk may legitimately
2246 /// target (LRM 9.6.2), besides a same-process lexically enclosing block
2247 /// or this CFG's own `fork_name`: seeded from [`Inherited::
2248 /// known_fork_labels`] (what the parent already knew, plus this fork's
2249 /// siblings, when this CFG is itself a branch), then extended, going
2250 /// forward, by every later fork THIS walk itself builds branches for --
2251 /// never popped (unlike `disable_targets`): LRM 9.6.2 lets a branch's
2252 /// name be disabled for the rest of the process's life, not only while
2253 /// the `fork` statement that spawned it is still being walked.
2254 fork_branch_names: Vec<String>,
2255 /// The named blocks enclosing the fork statement(s) this CFG is a branch of,
2256 /// in the forking process(es) (LRM 9.6.2): a `disable` of one from inside
2257 /// the branch ends that block's whole activity, the forking process
2258 /// suspended at the join included, which resumes after the block.
2259 ancestor_blocks: Vec<String>,
2260 /// The name of the process's outermost scope (LRM 16.14.6.4), if it is a
2261 /// named block: a `disable` of it flushes the pending procedural assertion
2262 /// queue.
2263 outermost: Option<String>,
2264 /// The labels of the procedural concurrent assertions of this process: a
2265 /// `disable <label>;` clears the pending instances of that assertion
2266 /// (16.14.6.4).
2267 assertion_labels: Vec<String>,
2268 /// Labels of the process's deferred immediate assertions: `disable L;`
2269 /// cancels their pending reports (16.4.4), codegen lowers the kept statement.
2270 deferred_labels: Vec<String>,
2271 /// Labels of the process's plain immediate assertions: `disable L;` from
2272 /// outside the labeled statement is a no-op (it already finished).
2273 immediate_labels: Vec<String>,
2274 /// The outermost named blocks of the instance's other processes, which this
2275 /// process may `disable` (16.14.6.4, the `b3` example).
2276 peers: Vec<PeerBlock>,
2277 /// [`ProcessCfg::captures`].
2278 captures: Vec<Expression<'d>>,
2279 /// The instance's default clocking block, which `##N` counts.
2280 default_clocking: Option<Symbol<'d>>,
2281 /// While lowering the timing control of a clocking drive (`cb.q <= ##N v`):
2282 /// the drive target's own clocking block, whose event `##N` counts (LRM
2283 /// 1800-2023 14.16) instead of the default clocking's.
2284 drive_clocking: Option<Symbol<'d>>,
2285 /// The clock variable (`v.sb.c`) of that drive when it goes through a
2286 /// virtual interface member access, else `None` (see
2287 /// [`SuspendPoint::CycleDelay`]'s `via`).
2288 drive_via: Option<Expression<'d>>,
2289 /// Set by [`Self::walk_timed`] around its [`Self::suspend_point`] call: a
2290 /// run-time `##(expr)` count is then accepted (as a single `##1` wait the
2291 /// caller wraps in a counted loop) and its expression left in
2292 /// `dynamic_cycle_count`; every other caller still refuses it by name.
2293 allow_dynamic_cycle: bool,
2294 /// Set by [`Self::walk_timed`] around the [`Self::suspend_point`] of a
2295 /// mixed class-event / signal `@(a or b)` list: the class terms are then
2296 /// skipped (the leaf statement queues the process on them) and only the
2297 /// signal terms form the suspend point. Every other caller keeps the class
2298 /// term, so it cannot be dropped silently.
2299 skip_class_event_terms: bool,
2300 dynamic_cycle_count: Option<Expression<'d>>,
2301 /// What a checker procedure's `@clock` waits on when `clock` is the default
2302 /// `$inferred_clock` ([`InferredClockEvent`]).
2303 checker_clock: Option<InferredClockEvent<'d>>,
2304 /// What `@$global_clock` waits on: the event of the global clocking in
2305 /// effect for the process (14.14), when one is declared in its instance.
2306 global_clock: Option<InferredClockEvent<'d>>,
2307 /// Walking the body of the plan that hosts an `expect` statement for the
2308 /// assertion engine ([`build_cfg_hosted_assertion`]): the `expect` is then
2309 /// an ordinary leaf, not a blocking wait.
2310 expect_as_leaf: bool,
2311 /// This CFG is the detached process of a `->> @(ctl) e;` (LRM 1800-2023
2312 /// 15.5.2): its body statement is that trigger, which waits on `ctl` and
2313 /// then stages the nonblocking trigger, while the issuing process
2314 /// continues. Cleared once the body statement is walked.
2315 detached_trigger: bool,
2316 /// The fork branches this process has built so far ([`ForkBranchKey`]):
2317 /// a task inlined at many call sites repeats the same forks, and each
2318 /// distinct branch is built (and later lowered) once.
2319 fork_memo: ForkMemo<'d>,
2320 /// This CFG is one branch of a call to an `extern forkjoin` task (LRM
2321 /// 25.7.5): its body statement is that call, and it runs this one
2322 /// implementation. Cleared once the body statement is walked.
2323 forced_extern: Option<Symbol<'d>>,
2324}
2325
2326impl<'d> CfgBuilder<'d> {
2327 fn new() -> Self {
2328 Self {
2329 expect_as_leaf: false,
2330 detached_trigger: false,
2331 fork_memo: ForkMemo::default(),
2332 forced_extern: None,
2333 blocks: Vec::new(),
2334 inferred_clock: None,
2335 next_repeat_counter: 0,
2336 inlined_locals: Vec::new(),
2337 ref_candidates: Vec::new(),
2338 entering_block: false,
2339 return_targets: Vec::new(),
2340 ret_var_stack: Vec::new(),
2341 this_stack: Vec::new(),
2342 ref_stack: Vec::new(),
2343 virtual_tasks: None,
2344 inline_stack: Vec::new(),
2345 capture_types: Vec::new(),
2346 rec: None,
2347 rec_up: Vec::new(),
2348 rec_outer: Vec::new(),
2349 loop_targets: Vec::new(),
2350 owned_scopes: Vec::new(),
2351 target_depth: std::collections::HashMap::new(),
2352 disable_targets: Vec::new(),
2353 body_node: None,
2354 fork_block_names: Vec::new(),
2355 body_forks: None,
2356 event_stack: Vec::new(),
2357 call_anchor: None,
2358 pending_result_temp: None,
2359 frame_stack: Vec::new(),
2360 scope_chain: Vec::new(),
2361 name_path: Vec::new(),
2362 block_scopes: Vec::new(),
2363 block_frames: Vec::new(),
2364 disable_broadcast: Vec::new(),
2365 ancestor_blocks: Vec::new(),
2366 fork_name: None,
2367 fork_branch_names: Vec::new(),
2368 outermost: None,
2369 assertion_labels: Vec::new(),
2370 deferred_labels: Vec::new(),
2371 immediate_labels: Vec::new(),
2372 peers: Vec::new(),
2373 captures: Vec::new(),
2374 default_clocking: None,
2375 drive_clocking: None,
2376 drive_via: None,
2377 allow_dynamic_cycle: false,
2378 skip_class_event_terms: false,
2379 dynamic_cycle_count: None,
2380 checker_clock: None,
2381 global_clock: None,
2382 }
2383 }
2384
2385 fn alloc_block(&mut self) -> BlockId {
2386 let id = BlockId(self.blocks.len() as u32);
2387 let this_var = self.this_stack.last().copied();
2388 let ref_aliases = self.ref_stack.clone();
2389 self.block_scopes.push(self.scope_chain.clone());
2390 self.block_frames.push(if self.scope_chain.is_empty() {
2391 Vec::new()
2392 } else {
2393 self.frame_stack.clone()
2394 });
2395 self.blocks.push(PartialBlock {
2396 bindings: Vec::new(),
2397 this_var,
2398 ref_aliases,
2399 stmts: Vec::new(),
2400 probes: Vec::new(),
2401 disable_probes: Vec::new(),
2402 frame_probes: Vec::new(),
2403 block_events: Vec::new(),
2404 stmt_scopes: Vec::new(),
2405 step_exprs: Vec::new(),
2406 repeat_init: None,
2407 repeat_decrement: None,
2408 expect_resume: false,
2409 terminator: None,
2410 });
2411 id
2412 }
2413
2414 /// After a statement that called a subroutine, in a design where some
2415 /// function has `disable <block>;` (LRM 9.6.2, 35.9): leaves the enclosing
2416 /// named block the call disabled, if any ([`SwitchOn::FnDisable`]).
2417 /// Returns the block the statements go on in.
2418 fn fn_disable_check(&mut self, cur: BlockId) -> BlockId {
2419 let targets: Vec<(usize, String, BlockId, usize)> = self
2420 .disable_targets
2421 .iter()
2422 .enumerate()
2423 .rev()
2424 .filter(|(_, (n, _, _))| is_function_disable_label(n))
2425 .map(|(pos, (n, exit, frames))| (pos, n.clone(), *exit, *frames))
2426 .collect();
2427 if targets.is_empty() {
2428 return cur;
2429 }
2430 let next = self.alloc_block();
2431 let mut arms = Vec::new();
2432 let mut labels = Vec::new();
2433 for (pos, label, exit, frames) in targets {
2434 let arm = self.alloc_block();
2435 // The inlined tasks the jump leaves end here, innermost first.
2436 let left: Vec<_> = self.frame_stack[frames..].iter().rev().cloned().collect();
2437 for (task, vars) in left {
2438 self.frame_probe(arm, FrameProbe::Exit { task, vars });
2439 }
2440 self.left_block_events(arm, pos);
2441 self.jump(arm, exit);
2442 arms.push(arm);
2443 labels.push(label);
2444 }
2445 self.close(
2446 cur,
2447 Terminator::Switch {
2448 on: SwitchOn::FnDisable { labels },
2449 arms,
2450 default_blk: next,
2451 report: None,
2452 },
2453 );
2454 next
2455 }
2456
2457 fn push(&mut self, blk: BlockId, stmt: Statement<'d>) {
2458 let scope = self.frame_stack.last().map(|f| f.0);
2459 let b = &mut self.blocks[blk.0 as usize];
2460 if let Some(scope) = scope {
2461 b.stmt_scopes.push((b.stmts.len(), scope));
2462 }
2463 b.stmts.push(stmt);
2464 }
2465
2466 /// Records that `stmt` is about to execute at the current end of block
2467 /// `blk`'s statements ([`BasicBlock::probes`]).
2468 fn probe(&mut self, blk: BlockId, stmt: Statement<'d>) {
2469 let b = &mut self.blocks[blk.0 as usize];
2470 let at = b.stmts.len();
2471 b.probes.push((at, stmt));
2472 }
2473
2474 /// Records that the `disable` statement `stmt`, turned into a jump, has
2475 /// taken effect at the current end of block `blk`'s statements
2476 /// ([`BasicBlock::disable_probes`]).
2477 fn disable_probe(&mut self, blk: BlockId, stmt: Statement<'d>) {
2478 let b = &mut self.blocks[blk.0 as usize];
2479 let at = b.stmts.len();
2480 b.disable_probes.push((at, stmt));
2481 }
2482
2483 /// Appends to `out` the `blocks` some `disable` of the design names (a
2484 /// fork branch's extra [`ProcessCfg::disable_names`]).
2485 fn disable_names_extra(&self, blocks: &[String], out: &mut Vec<String>) {
2486 for b in blocks {
2487 if is_disabled_somewhere(b) && !out.contains(b) {
2488 out.push(b.clone());
2489 }
2490 }
2491 }
2492
2493 /// Whether the body being walked holds a fork block, in itself or in a
2494 /// task it calls (cached).
2495 fn forks_in_body(&mut self) -> bool {
2496 if let Some(v) = self.body_forks {
2497 return v;
2498 }
2499 let v = self.body_node.is_some_and(contains_fork_through_calls);
2500 self.body_forks = Some(v);
2501 v
2502 }
2503
2504 /// `disable a.b;` (LRM 9.6.2) whose segments are a contiguous chain of
2505 /// this process's lexically enclosing named blocks: a jump to the last
2506 /// one's exit, exactly like `disable b;`. Any other hierarchical target
2507 /// (an instance path, a block not enclosing the statement, or the
2508 /// outermost block / a task frame, whose lowering needs the plain label)
2509 /// stays loud.
2510 fn walk_hier_disable_of_enclosing(
2511 &mut self,
2512 stmt: Statement<'d>,
2513 cur: BlockId,
2514 path: &[String],
2515 ) -> Result<Option<BlockId>, LoweringError> {
2516 let loud = || {
2517 LoweringError::unsupported(format!(
2518 "disable of `{}` (a hierarchical name is supported only as a chain of the \
2519 statement's own lexically enclosing named blocks, not the process's \
2520 outermost block or a task frame; IEEE 1800-2023 9.6.2)",
2521 path.join(".")
2522 ))
2523 };
2524 let last = path.last().ok_or_else(loud)?;
2525 let Some(pos) = self.disable_targets.iter().rposition(|(n, _, _)| n == last) else {
2526 return Ok(None);
2527 };
2528 let chain_ok = pos + 1 >= path.len()
2529 && path
2530 .iter()
2531 .enumerate()
2532 .all(|(i, seg)| self.disable_targets[pos + 1 - path.len() + i].0 == *seg);
2533 if !chain_ok {
2534 return Ok(None);
2535 }
2536 let (_, exit, frames) = self.disable_targets[pos].clone();
2537 let outermost = pos == 0 && self.outermost.as_deref() == Some(last.as_str());
2538 if outermost || frames > 0 {
2539 return Err(loud());
2540 }
2541 self.disable_probe(cur, stmt);
2542 self.left_block_events(cur, pos);
2543 self.jump(cur, exit);
2544 Ok(Some(self.alloc_block()))
2545 }
2546
2547 /// The target of `disable a.b.c;` (LRM 9.6.2) when every segment names a
2548 /// block of ANOTHER process of this instance ([`PeerBlock`]): the last
2549 /// segment, handled like the plain `disable c;` of that peer. An instance
2550 /// path (or any segment that is not a peer block) stays loud, since the
2551 /// runtime resolves a label within the requester's own instance only.
2552 fn hier_peer_leaf(&self, path: &[String]) -> Result<String, LoweringError> {
2553 if path
2554 .iter()
2555 .all(|seg| self.peers.iter().any(|p| &p.name == seg))
2556 {
2557 return Ok(path[path.len() - 1].clone());
2558 }
2559 // `disable outer.inner;` / `disable top.outer.inner;` where `inner` is
2560 // a fork branch's own named block (a sibling or a later branch of this
2561 // process) or a block enclosing the fork, reached through blocks of
2562 // this process family (LRM 9.6.2): the same target as the plain
2563 // `disable inner;`. A leading segment that names none of them is the
2564 // instance's own name (only when a block segment follows it). Left as
2565 // the dotted text it would name a block of a child instance and match
2566 // nothing, a silent no-op.
2567 let known = |s: &String| {
2568 self.ancestor_blocks.contains(s)
2569 || self.fork_branch_names.contains(s)
2570 || self.disable_targets.iter().any(|(n, _, _)| n == s)
2571 || self.outermost.as_deref() == Some(s.as_str())
2572 };
2573 if let Some((leaf, mid)) = path.split_last()
2574 && known(leaf)
2575 {
2576 let skip = usize::from(mid.len() >= 2 && !known(&mid[0]));
2577 if mid[skip..].iter().all(known) {
2578 return Ok(leaf.clone());
2579 }
2580 }
2581 // `disable u1.blk;` (an instance path): the first segment names
2582 // neither a block of this or another process nor a task, so it is a
2583 // child instance. Kept as the dotted text; the runtime resolves it from
2584 // the requester's instance (the extra scope keys run.rs registers).
2585 let first = &path[0];
2586 if !self.peers.iter().any(|p| &p.name == first)
2587 && !self.disable_targets.iter().any(|(n, _, _)| n == first)
2588 && !is_task_name(first)
2589 && self.fork_name.as_deref() != Some(first.as_str())
2590 && !self
2591 .fork_branch_names
2592 .iter()
2593 .any(|n| n == first && !self.fork_block_names.contains(n))
2594 {
2595 return Ok(path.join("."));
2596 }
2597 // `disable p.b.x;` where `x` also names a block elsewhere: the dotted
2598 // path from another process's outermost block `p`, which the runtime
2599 // finds under the path-qualified scope label (`CfgBuilder::name_path`).
2600 if self.peers.iter().any(|p| &p.name == first) {
2601 return Ok(path.join("."));
2602 }
2603 Err(LoweringError::unsupported(format!(
2604 "disable of `{}` (a hierarchical name is supported only as a chain of the \
2605 statement's own lexically enclosing named blocks or of the named blocks of \
2606 another process of the same instance, not an instance path, the process's \
2607 outermost block or a task frame; IEEE 1800-2023 9.6.2)",
2608 path.join(".")
2609 )))
2610 }
2611
2612 /// Records the frame probe `probe` at the current end of block `blk`'s
2613 /// statements ([`BasicBlock::frame_probes`]).
2614 fn frame_probe(&mut self, blk: BlockId, probe: FrameProbe<'d>) {
2615 let b = &mut self.blocks[blk.0 as usize];
2616 let at = b.stmts.len();
2617 b.frame_probes.push((at, probe));
2618 }
2619
2620 /// Records the covergroup block event of `task` (LRM 19.3) at the current
2621 /// end of block `blk`'s statements ([`BasicBlock::block_events`]).
2622 fn block_event(&mut self, blk: BlockId, task: Symbol<'d>, is_end: bool) {
2623 let b = &mut self.blocks[blk.0 as usize];
2624 let at = b.stmts.len();
2625 b.block_events.push((at, task, is_end));
2626 }
2627
2628 /// A `disable` jumping to `disable_targets[pos]` ends every block and
2629 /// inlined task entered inside that target (LRM 9.6.2): records their
2630 /// `end` covergroup events (LRM 19.3) at the jump, innermost first. The
2631 /// target's own end event is the one at its exit block.
2632 fn left_block_events(&mut self, cur: BlockId, pos: usize) {
2633 let left: Vec<_> = self
2634 .event_stack
2635 .iter()
2636 .rev()
2637 .take_while(|(_, depth)| *depth > pos)
2638 .map(|(s, _)| *s)
2639 .collect();
2640 for s in left {
2641 self.block_event(cur, s, true);
2642 }
2643 }
2644
2645 fn push_step_expr(&mut self, blk: BlockId, expr: Expression<'d>) {
2646 self.blocks[blk.0 as usize].step_exprs.push(expr);
2647 }
2648
2649 /// R-run Task 8: mints a fresh, process-unique [`RepeatCounterId`] for a
2650 /// suspend-containing `repeat(N)` loop's own synthetic counter.
2651 fn alloc_repeat_counter(&mut self) -> RepeatCounterId {
2652 let id = RepeatCounterId(self.next_repeat_counter);
2653 self.next_repeat_counter += 1;
2654 id
2655 }
2656
2657 /// R-run Task 8: records `blk`'s own `repeat_init` -- see
2658 /// [`BasicBlock::repeat_init`]'s doc comment. `blk` must not already
2659 /// carry one (a builder-internal consistency check, like [`Self::close`]'s
2660 /// own "not already closed" invariant: every call site is
2661 /// `walk_repeat_loop`'s own `cur`, which allocates/owns exactly one
2662 /// `repeat_init` per call).
2663 fn set_repeat_init(&mut self, blk: BlockId, counter: RepeatCounterId, count: RepeatCount<'d>) {
2664 let slot = &mut self.blocks[blk.0 as usize].repeat_init;
2665 debug_assert!(
2666 slot.is_none(),
2667 "cfg builder tried to set block {blk:?}'s repeat_init twice"
2668 );
2669 *slot = Some((counter, count));
2670 }
2671
2672 /// R-run Task 8: records `blk`'s own `repeat_decrement` -- see
2673 /// [`BasicBlock::repeat_decrement`]'s doc comment.
2674 fn set_repeat_decrement(&mut self, blk: BlockId, counter: RepeatCounterId) {
2675 let slot = &mut self.blocks[blk.0 as usize].repeat_decrement;
2676 debug_assert!(
2677 slot.is_none(),
2678 "cfg builder tried to set block {blk:?}'s repeat_decrement twice"
2679 );
2680 *slot = Some(counter);
2681 }
2682
2683 /// Records `signal`/`edge` (from a just-built `SuspendPoint::Edge`) as
2684 /// the process's inferred single clock -- see `inferred_clock`'s own doc
2685 /// comment. Compares *by referenced symbol identity*, since two
2686 /// `Expression` handles for the same `clk` are not otherwise comparable;
2687 /// an edge with no resolvable symbol (e.g. a bit-select or expression,
2688 /// not a plain name) can never disambiguate a later `##N`, so it does
2689 /// not overwrite an already-known clock but also never establishes one
2690 /// on its own. Task 6: a second sighting of the SAME signal with a
2691 /// DIFFERENT edge also becomes ambiguous now (`CycleDelay` carries a
2692 /// real `edge` field it must be unambiguous about, unlike the
2693 /// pre-Task-6 bare cycle count).
2694 fn note_edge_signal(&mut self, signal: Expression<'d>, edge: EdgeKind) {
2695 let this_sym = signal.referenced_symbol().map(|s| s.uid());
2696 match &self.inferred_clock {
2697 None => {
2698 self.inferred_clock = Some(this_sym.is_some().then_some((signal, edge)));
2699 }
2700 Some(Some((existing, existing_edge))) => {
2701 let existing_sym = existing.referenced_symbol().map(|s| s.uid());
2702 if existing_sym.is_none() || this_sym != existing_sym || *existing_edge != edge {
2703 self.inferred_clock = Some(None); // now ambiguous
2704 }
2705 }
2706 Some(None) => {} // already ambiguous; stays ambiguous
2707 }
2708 }
2709
2710 /// Closes `blk` with `term`. `blk` must not already be closed -- every
2711 /// call site in this file only ever passes a `BlockId` it just allocated
2712 /// or one a nested `walk_stmt` call returned (which is itself always
2713 /// still-open by the same invariant), so this is a builder-internal
2714 /// consistency check, not a condition reachable from malformed SV source.
2715 fn close(&mut self, blk: BlockId, term: Terminator<'d>) {
2716 let slot = &mut self.blocks[blk.0 as usize].terminator;
2717 debug_assert!(
2718 slot.is_none(),
2719 "cfg builder tried to close block {blk:?} twice"
2720 );
2721 *slot = Some(term);
2722 }
2723
2724 /// Closes `cur` with a jump to `target`, releasing on the way the handles
2725 /// of the block-scoped automatic variables the jump leaves (a `break`,
2726 /// `continue`, `return` or `disable` out of a block, LRM 6.21).
2727 fn jump(&mut self, cur: BlockId, target: BlockId) {
2728 let open = self.owned_scopes.len();
2729 let depth = self.target_depth.get(&target).copied().unwrap_or(open);
2730 let vars: Vec<Symbol<'d>> = self.owned_scopes[depth.min(open)..]
2731 .iter()
2732 .rev()
2733 .flat_map(|s| s.iter().rev().copied())
2734 .collect();
2735 if vars.is_empty() {
2736 self.close(cur, Terminator::Fallthrough(target));
2737 return;
2738 }
2739 let leave = self.alloc_block();
2740 self.close(cur, Terminator::Fallthrough(leave));
2741 for var in vars {
2742 self.blocks[leave.0 as usize]
2743 .bindings
2744 .push(ArgBinding::Release { var });
2745 }
2746 self.close(leave, Terminator::Fallthrough(target));
2747 }
2748
2749 /// The static variable or net a `ref` actual names: itself, or what an
2750 /// enclosing inlined call's aliased `ref` formal it names stands for.
2751 fn ref_storage(&self, actual: Expression<'d>) -> Option<Symbol<'d>> {
2752 if !crate::symid::is_value_name(actual.kind()) {
2753 return None;
2754 }
2755 let sym = actual.referenced_symbol()?;
2756 if sym.kind() == SymbolKind::Net
2757 || (sym.kind() == SymbolKind::Variable
2758 && sym.variable_lifetime() == sv_lang::VariableLifetime::Static)
2759 {
2760 return Some(sym);
2761 }
2762 self.ref_stack
2763 .iter()
2764 .rev()
2765 .find(|(f, _)| f.uid() == sym.uid())
2766 .map(|&(_, a)| a)
2767 }
2768
2769 /// Resolves [`Self::ref_candidates`] into `ProcessCfg::ref_bindings`: a
2770 /// `ref` formal every call binds to one static variable or net (directly,
2771 /// or through an enclosing inlined task's `ref` formal that is itself
2772 /// bound) names that storage, so its copy-in/copy-out bindings and its
2773 /// frame local are dropped. A formal bound to different actuals, or to
2774 /// an automatic variable or a select, keeps the copy semantics.
2775 fn take_ref_bindings(&mut self) -> Vec<(Symbol<'d>, Symbol<'d>)> {
2776 use std::collections::HashMap;
2777 let storage = |s: &Symbol<'d>| {
2778 s.kind() == SymbolKind::Net
2779 || (s.kind() == SymbolKind::Variable
2780 && s.variable_lifetime() == sv_lang::VariableLifetime::Static)
2781 };
2782 // formal uid -> (formal, the one target symbol every call names), or
2783 // None once two calls disagree or one names no plain symbol.
2784 let mut targets: HashMap<crate::symid::SymbolId, (Symbol<'d>, Option<Symbol<'d>>)> =
2785 HashMap::new();
2786 let mut order = Vec::new();
2787 for (formal, named) in &self.ref_candidates {
2788 match targets.get_mut(&formal.uid()) {
2789 None => {
2790 order.push(formal.uid());
2791 targets.insert(formal.uid(), (*formal, *named));
2792 }
2793 Some((_, cur)) => {
2794 if cur.map(|c| c.uid()) != named.map(|n| n.uid()) {
2795 *cur = None;
2796 }
2797 }
2798 }
2799 }
2800 // Follow a chain through enclosing tasks' bound `ref` formals.
2801 let resolve = |start: Symbol<'d>| -> Option<Symbol<'d>> {
2802 let mut cur = start;
2803 for _ in 0..=targets.len() {
2804 if storage(&cur) {
2805 return Some(cur);
2806 }
2807 cur = targets.get(&cur.uid())?.1?;
2808 }
2809 None
2810 };
2811 let mut out = Vec::new();
2812 for uid in order {
2813 let (formal, named) = targets[&uid];
2814 if let Some(actual) = named.and_then(resolve) {
2815 out.push((formal, actual));
2816 }
2817 }
2818 let bound: std::collections::HashSet<crate::symid::SymbolId> =
2819 out.iter().map(|(f, _)| f.uid()).collect();
2820 for b in &mut self.blocks {
2821 b.bindings.retain(|x| match x {
2822 ArgBinding::In { formal, .. } | ArgBinding::Out { formal, .. } => {
2823 !bound.contains(&formal.uid())
2824 }
2825 _ => true,
2826 });
2827 }
2828 self.inlined_locals.retain(|l| !bound.contains(&l.uid()));
2829 out
2830 }
2831
2832 /// [`ProcessCfg::disable_scopes`] from the scopes each block was allocated in.
2833 ///
2834 /// A resume point inside inlined tasks the disabled scope does not contain
2835 /// goes through a block of its own that ends their VPI frames first,
2836 /// innermost first (37.43), like a `disable` jump inside the process does.
2837 fn take_disable_scopes(&mut self) -> Vec<(BlockId, String, BlockId)> {
2838 let scopes = std::mem::take(&mut self.block_scopes);
2839 let frames = std::mem::take(&mut self.block_frames);
2840 let mut out = Vec::new();
2841 for (i, (chain, active)) in scopes.into_iter().zip(frames).enumerate() {
2842 for (label, exit, outside) in chain {
2843 let target = if active.len() > outside {
2844 let leave = self.alloc_block();
2845 for (task, vars) in active[outside..].iter().rev().cloned() {
2846 self.frame_probe(leave, FrameProbe::Exit { task, vars });
2847 }
2848 self.close(leave, Terminator::Fallthrough(exit));
2849 leave
2850 } else {
2851 exit
2852 };
2853 out.push((BlockId(i as u32), label, target));
2854 }
2855 }
2856 out
2857 }
2858
2859 fn finish(self) -> Vec<BasicBlock<'d>> {
2860 self.blocks
2861 .into_iter()
2862 .map(|b| BasicBlock {
2863 bindings: b.bindings,
2864 this_var: b.this_var,
2865 ref_aliases: b.ref_aliases,
2866 stmts: b.stmts,
2867 probes: b.probes,
2868 disable_probes: b.disable_probes,
2869 frame_probes: b.frame_probes,
2870 block_events: b.block_events,
2871 stmt_scopes: b.stmt_scopes,
2872 step_exprs: b.step_exprs,
2873 repeat_init: b.repeat_init.clone(),
2874 repeat_decrement: b.repeat_decrement,
2875 expect_resume: b.expect_resume,
2876 terminator: b.terminator.expect(
2877 "internal invariant: build_cfg must close every block it allocates \
2878 before returning Ok",
2879 ),
2880 })
2881 .collect()
2882 }
2883
2884 /// PR-007's single recursive-descent walk. Returns the [`BlockId`] that
2885 /// is "current" for whatever statement follows `stmt` in its enclosing
2886 /// sequence. Every construct reachable in the A2 slice's restricted
2887 /// grammar falls through -- there is no `return`/`disable`/`break`/
2888 /// `continue` meaningfully reachable from a process body here (loops,
2889 /// functions and tasks are out of slice scope), so this never needs to
2890 /// signal "does not fall through".
2891 ///
2892 /// `sv_lang::kinds::StatementKind` is `#[non_exhaustive]` upstream (a
2893 /// `sv-lang-kinds` version bump can add a variant without breaking this
2894 /// crate's build), so a literal Rust `match` over it cannot itself be
2895 /// exhaustive-without-a-wildcard -- the compiler mandates a catch-all
2896 /// arm. Per this workspace's already-established pattern for these
2897 /// generated kind enums (`vj-codegen::module`'s `TimingControlKind`/
2898 /// `StatementKind` handling, `determinism::kind_guard`'s doc comment),
2899 /// this is instead an `if`/`else-if` chain that names every one of
2900 /// today's 32 variants explicitly (`StatementKind::COUNT`, pinned by
2901 /// `determinism::kind_guard::statement_kind_count_matches_pinned_sv_lang_
2902 /// version`), falling through to a named [`LoweringError`] -- never a
2903 /// silent no-op -- only if a future version bump adds a 33rd variant
2904 /// this file has not been reviewed against (which that pinned-count test
2905 /// catches first).
2906 fn walk_stmt(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
2907 // A statically dead `if`/`case` arm slang could not bind demotes the
2908 // enclosing statements to `Invalid` wrappers; the real statement is
2909 // recoverable (see [`unwrap_invalid`]).
2910 let stmt = unwrap_invalid(stmt);
2911 let kind = stmt.kind();
2912
2913 if kind == StatementKind::Invalid {
2914 return Err(LoweringError::malformed(
2915 "Invalid statement node -- slang rejected this statement; an error inside a \
2916 statically dead `if`/`case` arm (e.g. a reversed range select `v[7:8]` of a \
2917 descending vector under a false parameter condition) is suppressed by slang \
2918 but still invalidates the whole enclosing procedure",
2919 ));
2920 }
2921 if kind == StatementKind::List {
2922 return self.walk_seq(live_statements(&stmt), cur);
2923 }
2924 // VPI `cbStmt` (38.36.1, Table 38-6): the statement is about to
2925 // execute. The detached process of `->> @(ctl) e;` runs the same
2926 // statement the issuer already probed. Loops are probed per iteration
2927 // by their own walkers (or by codegen when they stay a leaf).
2928 if is_entry_probe_kind(&stmt) && !self.detached_trigger {
2929 self.probe(cur, stmt);
2930 }
2931 if kind == StatementKind::Block {
2932 return self.walk_block(stmt, cur);
2933 }
2934 if kind == StatementKind::Conditional {
2935 return self.walk_conditional(stmt, cur);
2936 }
2937 if kind == StatementKind::Timed {
2938 return self.walk_timed(stmt, cur);
2939 }
2940 if kind == StatementKind::Wait {
2941 return self.walk_wait(stmt, cur);
2942 }
2943 if kind == StatementKind::WaitFork {
2944 // SC-125: a real suspend -- the scheduler resumes the process once
2945 // every child it forked has finished (`Suspend::WaitFork`).
2946 let resume = self.alloc_block();
2947 self.close(
2948 cur,
2949 Terminator::Suspend {
2950 point: SuspendPoint::WaitFork,
2951 resume_blk: resume,
2952 },
2953 );
2954 return Ok(resume);
2955 }
2956 if kind == StatementKind::ExpressionStatement && is_process_by_request_stmt(&stmt) {
2957 // Classes plan Task 13: `p.await();` / `p.suspend();`. The call
2958 // (an ordinary leaf statement) queues the request; the suspend
2959 // after it lets the scheduler apply it -- block in `await`,
2960 // park a self-suspend -- before the next statement runs.
2961 self.push(cur, stmt);
2962 let resume = self.alloc_block();
2963 self.close(
2964 cur,
2965 Terminator::Suspend {
2966 point: SuspendPoint::ByRequest,
2967 resume_blk: resume,
2968 },
2969 );
2970 // Classes plan Task 18: a mailbox `get`/`peek` delivers its message
2971 // into a variable, which a process that blocked cannot have done
2972 // when the call returned. The statement runs a second time after
2973 // the resume; the runtime tells the two phases apart (the first one
2974 // of a call that did not block leaves a marker, so the second is a
2975 // no-op: `vj_sim::sched::mailbox`).
2976 if is_mailbox_two_phase_stmt(&stmt) {
2977 self.push(resume, stmt);
2978 }
2979 return Ok(resume);
2980 }
2981 if kind == StatementKind::WaitOrder {
2982 return self.walk_wait_order(stmt, cur);
2983 }
2984 if is_expect(&stmt) && !self.expect_as_leaf {
2985 return self.walk_expect(stmt, cur);
2986 }
2987 if kind == StatementKind::RandCase
2988 && (contains_suspend_trigger(stmt.as_sem_node())
2989 || escapes(
2990 stmt.as_sem_node(),
2991 !self.loop_targets.is_empty(),
2992 !self.return_targets.is_empty(),
2993 ))
2994 {
2995 return self.walk_randcase(stmt, cur);
2996 }
2997 if kind == StatementKind::EventTrigger {
2998 return self.walk_event_trigger(stmt, cur);
2999 }
3000 if kind == StatementKind::ExpressionStatement && async_pla_call(&stmt).is_some() {
3001 return self.walk_async_pla(stmt, cur);
3002 }
3003 if kind == StatementKind::DisableFork {
3004 // SC-125: never suspends -- an ordinary straight-line statement
3005 // that codegen lowers to a `sv_rt_disable_fork` call.
3006 self.push(cur, stmt);
3007 return Ok(cur);
3008 }
3009 if kind == StatementKind::Disable {
3010 // `disable blk;` inside `blk`: leave the block, like `break`
3011 // leaves a loop (LRM 1800-2023 §9.6.2). The target is read from
3012 // the statement's source text (`disable_label`).
3013 // `disable outer.inner;` (9.6.2 hierarchical block name) naming a
3014 // chain of this process's own lexically enclosing blocks is the
3015 // same jump as `disable inner;`.
3016 let mut label_override = None;
3017 if let Some(path) = disable_path(&stmt) {
3018 if let Some(next) = self.walk_hier_disable_of_enclosing(stmt, cur, &path)? {
3019 return Ok(next);
3020 }
3021 // A block nested in another process's scope, named through
3022 // that scope's blocks (`disable p.inner;`).
3023 label_override = Some(self.hier_peer_leaf(&path)?);
3024 }
3025 let label = match label_override {
3026 Some(l) => l,
3027 None => disable_label(&stmt)?,
3028 };
3029 // `disable f;` inside a branch of fork `f`: ends every branch
3030 // (codegen's runtime call), this one included.
3031 if !self.disable_targets.iter().any(|(n, _, _)| *n == label)
3032 && self.fork_name.as_deref() == Some(label.as_str())
3033 {
3034 self.push(cur, stmt);
3035 self.close(cur, Terminator::End { loop_back: None });
3036 return Ok(self.alloc_block());
3037 }
3038 if let Some(pos) = self
3039 .disable_targets
3040 .iter()
3041 .rposition(|(n, _, _)| *n == label)
3042 {
3043 let (_, exit, frames) = self.disable_targets[pos].clone();
3044 // 16.14.6.4: disabling the process's outermost scope also
3045 // flushes its pending procedural assertion queue; a nested
3046 // block does not. Codegen turns the kept statement into the
3047 // flush.
3048 let flush = pos == 0 && self.outermost.as_deref() == Some(label.as_str());
3049 // A task activation, or a block inside one, can be live in
3050 // other processes too (LRM 9.6.2: every activation ends): the
3051 // kept statement also reaches them at run time.
3052 let broadcast = frames > 0;
3053 // LRM 9.6.2: leaving the block ends the activities enabled
3054 // within it, the fork branches still running among them
3055 // (tagged with the block's name, `disable_names`).
3056 let ends_branches =
3057 !broadcast && self.forks_in_body() && is_disabled_somewhere(&label);
3058 if flush || broadcast || ends_branches {
3059 self.push(cur, stmt);
3060 }
3061 if broadcast || ends_branches {
3062 self.disable_broadcast.push((label.clone(), false));
3063 }
3064 self.disable_probe(cur, stmt);
3065 // The inlined tasks the jump leaves end here, innermost first.
3066 let left: Vec<_> = self.frame_stack[frames..].iter().rev().cloned().collect();
3067 for (task, vars) in left {
3068 self.frame_probe(cur, FrameProbe::Exit { task, vars });
3069 }
3070 self.left_block_events(cur, pos);
3071 self.jump(cur, exit);
3072 return Ok(self.alloc_block());
3073 }
3074 // `disable a1;` of a procedural concurrent assertion's label
3075 // (16.14.6.4) clears that assertion's pending instances and goes
3076 // on: the statement is kept for codegen.
3077 if self.assertion_labels.contains(&label) {
3078 self.push(cur, stmt);
3079 return Ok(cur);
3080 }
3081 // `disable a1;` of a deferred immediate assertion's label (16.4.4):
3082 // cancels its pending reports and goes on; kept for codegen.
3083 if self.deferred_labels.contains(&label) {
3084 self.push(cur, stmt);
3085 return Ok(cur);
3086 }
3087 // `disable L;` of a plain immediate assertion's label from outside
3088 // its statement: the statement has finished, nothing to end.
3089 if self.immediate_labels.contains(&label) {
3090 return Ok(cur);
3091 }
3092 // `disable b2;` of another process's outermost block (16.14.6.4,
3093 // the `b3` example): the other process cannot be inside a block
3094 // without a suspend point, so the disable's whole effect is that
3095 // process's queue flush.
3096 if let Some(can_suspend) = self
3097 .peers
3098 .iter()
3099 .find(|p| p.name == label)
3100 .map(|p| p.can_suspend)
3101 {
3102 self.push(cur, stmt);
3103 // A block with a suspend point may have its process inside it:
3104 // the disable then resumes that process after the block
3105 // (`sv_rt_disable_named`, `ProcessCfg::disable_scopes`).
3106 if can_suspend {
3107 self.disable_broadcast.push((label, true));
3108 }
3109 return Ok(cur);
3110 }
3111 // `disable u1.blk;` of a block of a child instance: every process
3112 // of that instance suspended inside the block resumes after it.
3113 if label.contains('.') {
3114 self.push(cur, stmt);
3115 self.disable_broadcast.push((label, true));
3116 return Ok(cur);
3117 }
3118 // `disable t;` of a task of the design that this process is not in:
3119 // it ends every activation of `t` in the other processes (LRM
3120 // 9.6.2) and the process goes on.
3121 if is_task_name(&label) {
3122 self.push(cur, stmt);
3123 self.disable_broadcast.push((label, true));
3124 return Ok(cur);
3125 }
3126 // LRM 9.6.2: `label` is a fork branch's own named outermost
3127 // block reachable from here -- the fork's parent (continuing
3128 // past it), a sibling branch, or a descendant of an ancestor
3129 // fork -- but not through a same-process lexically enclosing
3130 // block or this CFG's own `fork_name` (both already checked
3131 // above). Kept as an ordinary statement: codegen (`vj-
3132 // codegen::process_lower`) tells this case apart from the ones
3133 // above by `ProcessLoweringPlan::fork_branch_names` and lowers
3134 // it to a runtime scan/kill of every live, so-tagged process
3135 // reachable from the caller's own fork family
3136 // (`vj_sim::sched::SchedulerOps::disable_named`) -- never a
3137 // silent no-op when nothing is currently live (the same
3138 // "already finished, nothing to do" convention `disable fork;`
3139 // itself uses, LRM 9.6.3).
3140 if self.fork_branch_names.iter().any(|n| n == &label) {
3141 self.push(cur, stmt);
3142 return Ok(cur);
3143 }
3144 // LRM 9.6.2: `label` is a named block of the forking process
3145 // that encloses the fork this branch belongs to. The disable
3146 // ends all activity in the block: the siblings, this branch, and
3147 // the parent's wait at the join, which resumes after the block
3148 // (`sv_rt_disable_named`, the parent's `disable_scopes`). This
3149 // branch ends here, like `disable <fork name>;` above.
3150 if self.ancestor_blocks.iter().any(|n| n == &label) {
3151 self.push(cur, stmt);
3152 self.disable_broadcast.push((label, true));
3153 self.close(cur, Terminator::End { loop_back: None });
3154 return Ok(self.alloc_block());
3155 }
3156 return Err(LoweringError::unsupported(format!(
3157 "disable {label}; where `{label}` is not a named block or task enclosing it \
3158 in the same process, a procedural assertion label of it, a uniquely named \
3159 sequential block of another process or a task of the design (not a block \
3160 inside a fork branch of another process, nor a name shared by two blocks), \
3161 or a fork branch's own name reachable from here"
3162 )));
3163 }
3164 if kind == StatementKind::ForLoop {
3165 return self.walk_for_loop(stmt, cur);
3166 }
3167 if kind == StatementKind::WhileLoop {
3168 return self.walk_while_loop(stmt, cur);
3169 }
3170 if kind == StatementKind::DoWhileLoop {
3171 return self.walk_do_while_loop(stmt, cur);
3172 }
3173 if kind == StatementKind::ForeverLoop {
3174 return self.walk_forever_loop(stmt, cur);
3175 }
3176 if kind == StatementKind::ForeachLoop {
3177 return self.walk_foreach_loop(stmt, cur);
3178 }
3179 // R-run Task 8 (PR-045/PR-006): `repeat(N) body;` gets a real CFG
3180 // `Loop` shape ONLY when `body` contains a suspend point somewhere
3181 // in its own subtree -- a suspend inside a `repeat` needs its own
3182 // iteration counter to survive the resulting suspend/resume call
3183 // boundary (see `RepeatCounterId`'s own doc comment), which only a
3184 // real CFG split can provide. The suspend-free case stays exactly
3185 // the pre-existing opaque leaf `is_ordinary_statement_kind` already
3186 // routes to `stmt_shared::lower_repeat_loop` (R-run Task 4) --
3187 // falling through to that check below, unchanged, whenever this
3188 // condition is false, deliberately preserving
3189 // `repeat_loop_control_flow_shape_matches_between_function_and_
3190 // process_paths`'s own "a suspend-free repeat loop stays an
3191 // ordinary, un-decomposed leaf statement" invariant
3192 // (`shared_stmt_lowering.rs`).
3193 if kind == StatementKind::RepeatLoop
3194 && (contains_suspend_trigger(stmt.as_sem_node())
3195 || escapes(stmt.as_sem_node(), false, !self.return_targets.is_empty()))
3196 {
3197 return self.walk_repeat_loop(stmt, cur);
3198 }
3199
3200 // R-run Task 1: `$finish`/`$stop`/`$fatal` are a segment-closing
3201 // point, same category as a suspend -- LRM 1800-2023 §20.2/§20.10:
3202 // once one of these executes, none of the calling process's own
3203 // textually-following statements may ever run (not just the next
3204 // one in a flat statement list -- also not one nested arbitrarily
3205 // deep inside an `if`/`case`/loop branch this "ordinary straight-
3206 // line" arm below would otherwise happily keep pushing into the
3207 // SAME still-open block). This is checked before the generic
3208 // `is_ordinary_statement_kind` dispatch below (an `ExpressionStatement`
3209 // wrapping one of these calls is itself an ordinary-kind statement,
3210 // so without this earlier check it would fall straight into that
3211 // arm and keep accumulating every statement that follows it into
3212 // `cur`).
3213 //
3214 // Reuses the existing `Terminator::End { loop_back: None }` shape
3215 // (PR-069's "no new enum catch-all" extension policy) rather than a
3216 // new `Terminator::Halt`/`Terminator::Finish` variant: this closes
3217 // `cur` as a real process-kernel exit (`SegTerminator::End{loop_back:
3218 // None}` -> the compiled kernel returns `SEG_FINISHED` for this
3219 // call, per `lower_terminator`'s existing 1:1 mapping), unconditionally
3220 // `loop_back: None` even inside an `always`/`always_ff` process
3221 // (`ProcessLoopKind::Looping` would otherwise close the process's
3222 // FINAL block with `Some(entry)`) -- once `$finish`/`$stop`/`$fatal`
3223 // runs, the calling process itself must never be re-entered again,
3224 // regardless of what kind of process it is; the *global* simulation
3225 // shutdown that a bare `$finish`/`$stop` also requests is a
3226 // scheduler-level concern (`SchedulerOps::request_finish`,
3227 // `sv_rt_finish`'s own doc comment) this file has no visibility
3228 // into and does not need to model.
3229 //
3230 // Any statement textually following the call (whether later in this
3231 // same flat list, as `walk_seq`'s next loop iteration, or -- via
3232 // this function's own caller chain, e.g. `walk_conditional` closing
3233 // `then_exit`/`else_exit` with `Terminator::Fallthrough(join)` --
3234 // structurally nested deeper) is walked into a FRESH block instead
3235 // of `cur`: `cur` is already closed, so it can never be reached
3236 // again, and neither can anything later routed into the fresh
3237 // block, since nothing in this builder ever branches to a block
3238 // this function itself did not just hand back as "the current
3239 // exit" to some earlier, still-open predecessor -- there is none
3240 // here. The fresh block still gets a real terminator from whatever
3241 // caller eventually closes it (a plain, harmless `Fallthrough`/
3242 // `End` on dead code), satisfying `finish()`'s "every block must be
3243 // closed" invariant without special-casing unreachable blocks.
3244 // A call to a task whose body can suspend is inlined here, so its
3245 // timing controls become real suspend points of this process.
3246 if kind == StatementKind::ExpressionStatement
3247 && let Some(e) = stmt.expr()
3248 && crate::netlist::itf::is_vif_extern_call(&e)
3249 {
3250 return Err(LoweringError::unsupported(
3251 crate::netlist::itf::vif_extern_message(&e),
3252 ));
3253 }
3254 if kind == StatementKind::ExpressionStatement
3255 && let Some(e) = stmt.expr()
3256 && let Some(targets) = crate::netlist::itf::call_extern_targets(&e)
3257 && let Some(next) = self.walk_extern_call(stmt, e, &targets, cur)?
3258 {
3259 return Ok(next);
3260 }
3261 if kind == StatementKind::ExpressionStatement
3262 && let Some(e) = stmt.expr()
3263 && let Some(callee) = suspending_task_callee(&e)
3264 {
3265 return self.inline_task_call(e, callee, cur);
3266 }
3267 if matches!(kind, StatementKind::Break | StatementKind::Continue) {
3268 let &(cont, brk) = self
3269 .loop_targets
3270 .last()
3271 .ok_or_else(|| LoweringError::malformed(format!("{kind:?} outside any loop")))?;
3272 let target = if kind == StatementKind::Break {
3273 brk
3274 } else {
3275 cont
3276 };
3277 self.jump(cur, target);
3278 return Ok(self.alloc_block());
3279 }
3280 if (kind == StatementKind::Case || kind == StatementKind::PatternCase)
3281 && (contains_suspend_trigger(stmt.as_sem_node())
3282 || constant_case_live_arm(&stmt).is_some()
3283 || escapes(
3284 stmt.as_sem_node(),
3285 !self.loop_targets.is_empty(),
3286 !self.return_targets.is_empty(),
3287 ))
3288 {
3289 return self.walk_case(stmt, cur);
3290 }
3291 if kind == StatementKind::ImmediateAssertion
3292 && let Some(actions) = suspending_assertion_actions(&stmt)
3293 {
3294 return self.walk_immediate_assertion(stmt, actions, cur);
3295 }
3296 if kind == StatementKind::ExpressionStatement
3297 && let Some(e) = stmt.expr()
3298 && e.kind() == ExpressionKind::Assignment
3299 && e.is_nonblocking() != Some(true)
3300 && e.assignment_timing().is_some()
3301 {
3302 return self.walk_intra_assignment_timing(e, cur);
3303 }
3304 // `q <= @(e) v;` / `q <= repeat (n) @(e) v;` (LRM 10.4.2). A plain
3305 // delay stays a statement: the staged write carries it.
3306 if kind == StatementKind::ExpressionStatement
3307 && let Some(e) = stmt.expr()
3308 && e.kind() == ExpressionKind::Assignment
3309 && e.is_nonblocking() == Some(true)
3310 && e.assignment_timing()
3311 .and_then(|n| n.as_timing_control())
3312 .is_some_and(|tc| tc.kind() != TimingControlKind::Delay)
3313 {
3314 return self.walk_nba_event(stmt, e, cur);
3315 }
3316 if kind == StatementKind::Return
3317 && let Some(&target) = self.return_targets.last()
3318 {
3319 // `return;` inside an inlined task: jump past the task body.
3320 // `return expr;` of an inlined function first assigns its result
3321 // variable (in a block of its own: bindings run on block entry).
3322 let mut cur = cur;
3323 if self.ret_var_stack.len() == self.return_targets.len()
3324 && let Some(Some(rv)) = self.ret_var_stack.last().copied()
3325 && let Some(value) = stmt.expr()
3326 {
3327 if let Some(next) = self.hoist_suspending_calls_in(value.as_sem_node(), cur)? {
3328 cur = next;
3329 }
3330 let set = self.alloc_block();
3331 self.close(cur, Terminator::Fallthrough(set));
3332 self.blocks[set.0 as usize].bindings.push(ArgBinding::In {
3333 formal: rv,
3334 actual: value,
3335 });
3336 cur = set;
3337 }
3338 self.jump(cur, target);
3339 return Ok(self.alloc_block());
3340 }
3341 if kind == StatementKind::ExpressionStatement && is_finish_or_stop_call(&stmt) {
3342 self.push(cur, stmt);
3343 self.close(cur, Terminator::End { loop_back: None });
3344 return Ok(self.alloc_block());
3345 }
3346
3347 // PR-006's own acceptance text: "any other kind is ordinary
3348 // straight-line code". Kept as one opaque `Statement` leaf unless it
3349 // secretly contains a suspend-triggering construct somewhere in its
3350 // subtree, in which case there is no `Terminator::Loop`/`Case` to
3351 // represent the needed split (see this file's module doc comment's
3352 // extension policy) -- a named `LoweringError`, never a
3353 // silently-wrong CFG.
3354 if is_ordinary_statement_kind(kind) {
3355 if kind == StatementKind::ExpressionStatement
3356 && contains_suspend_trigger(stmt.as_sem_node())
3357 && let Some(next) = self.hoist_suspending_function_calls(stmt, cur)?
3358 {
3359 return Ok(next);
3360 }
3361 return if contains_suspend_trigger(stmt.as_sem_node())
3362 && !(is_expect(&stmt) && self.expect_as_leaf)
3363 {
3364 Err(LoweringError::unsupported(format!(
3365 "suspend point nested inside a {kind:?} statement (no CFG terminator \
3366 for this construct is implemented in this task, PR-069)"
3367 )))
3368 } else {
3369 self.push(cur, stmt);
3370 if kind == StatementKind::ExpressionStatement
3371 && has_function_disables()
3372 && contains_subroutine_call(stmt.as_sem_node())
3373 {
3374 return Ok(self.fn_disable_check(cur));
3375 }
3376 Ok(cur)
3377 };
3378 }
3379
3380 // Unreachable as long as `determinism::kind_guard`'s pinned
3381 // `StatementKind::COUNT == 32` still holds -- every one of today's 32
3382 // variants is named above or in `is_ordinary_statement_kind`. A
3383 // `sv-lang-kinds` bump that adds a 33rd fails that pinned-count test
3384 // loudly; until this file is reviewed and updated, reaching here is a
3385 // named error, never a silent fallthrough.
3386 Err(LoweringError::malformed(format!(
3387 "unrecognized StatementKind {kind:?} (sv-lang-kinds version drift -- see \
3388 determinism::kind_guard's StatementKind pin)"
3389 )))
3390 }
3391
3392 /// `expect (property) pass else fail;` (LRM 1800-2023 §16.17): blocks the
3393 /// process until the assertion engine has decided the property.
3394 ///
3395 /// The statement appears twice, as the assertion's two halves. In the block
3396 /// it is reached in, it queues the evaluation (codegen's
3397 /// `sv_rt_proc_assert`); the process then parks with no sensitivity at all
3398 /// (an empty [`SuspendPoint::EdgeList`]) until the engine, in the Observed
3399 /// region of the step it decides in, wakes it. In the block it resumes into
3400 /// ([`BasicBlock::expect_resume`]) it runs the pass or `else` statement, and
3401 /// whatever follows the `expect` goes on from there.
3402 fn walk_expect(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
3403 let blocking = [stmt.assertion_if_true(), stmt.assertion_if_false()]
3404 .into_iter()
3405 .flatten()
3406 .any(|a| contains_suspend_trigger(a.as_sem_node()));
3407 if blocking {
3408 return self.walk_expect_blocking(stmt, cur);
3409 }
3410 self.push(cur, stmt);
3411 let resume = self.alloc_block();
3412 self.blocks[resume.0 as usize].expect_resume = true;
3413 self.close(
3414 cur,
3415 Terminator::Suspend {
3416 point: SuspendPoint::EdgeList(Vec::new(), Vec::new()),
3417 resume_blk: resume,
3418 },
3419 );
3420 self.push(resume, stmt);
3421 Ok(resume)
3422 }
3423
3424 /// An `expect` whose pass or `else` action blocks (a delay, an event
3425 /// control, a wait, another `expect`; LRM 1800-2023 16.17 allows any
3426 /// statement there). The action cannot run inline in the resume
3427 /// segment, so the resume block switches on the outcome the engine woke
3428 /// the process with (`sv_rt_fired_term`: 0 pass, 1 fail, 2 -- actions
3429 /// switched off by `$assertcontrol`, 20.11 -- neither) and walks each
3430 /// action as ordinary blocks. A fail with no `else` reports the `$error`
3431 /// through a zero-arm switch, as `wait_order` does.
3432 fn walk_expect_blocking(
3433 &mut self,
3434 stmt: Statement<'d>,
3435 cur: BlockId,
3436 ) -> Result<BlockId, LoweringError> {
3437 self.push(cur, stmt);
3438 let resume = self.alloc_block();
3439 self.close(
3440 cur,
3441 Terminator::Suspend {
3442 point: SuspendPoint::EdgeList(Vec::new(), Vec::new()),
3443 resume_blk: resume,
3444 },
3445 );
3446 let join = self.alloc_block();
3447 let pass = self.alloc_block();
3448 let fail = self.alloc_block();
3449 let pass_exit = match stmt.assertion_if_true() {
3450 Some(s) => self.walk_stmt(s, pass)?,
3451 None => pass,
3452 };
3453 self.close(pass_exit, Terminator::Fallthrough(join));
3454 let fail_exit = match stmt.assertion_if_false() {
3455 Some(s) => self.walk_stmt(s, fail)?,
3456 None => {
3457 self.close(
3458 fail,
3459 Terminator::Switch {
3460 on: SwitchOn::FiredTerm,
3461 arms: Vec::new(),
3462 default_blk: join,
3463 report: Some(stmt.source_range().start),
3464 },
3465 );
3466 self.alloc_block()
3467 }
3468 };
3469 self.close(fail_exit, Terminator::Fallthrough(join));
3470 self.close(
3471 resume,
3472 Terminator::Switch {
3473 on: SwitchOn::FiredTerm,
3474 arms: vec![pass, fail, join],
3475 default_blk: join,
3476 report: None,
3477 },
3478 );
3479 Ok(join)
3480 }
3481
3482 /// A `randcase` whose items need their own blocks (one suspends, or
3483 /// escapes an enclosing loop or task): [`SwitchOn::RandCase`] picks one.
3484 fn walk_randcase(
3485 &mut self,
3486 stmt: Statement<'d>,
3487 cur: BlockId,
3488 ) -> Result<BlockId, LoweringError> {
3489 let join = self.alloc_block();
3490 let mut arms = Vec::new();
3491 for item in stmt.randcase_items() {
3492 let arm = self.alloc_block();
3493 arms.push(arm);
3494 let exit = self.walk_stmt(item.stmt(), arm)?;
3495 self.close(exit, Terminator::Fallthrough(join));
3496 }
3497 self.close(
3498 cur,
3499 Terminator::Switch {
3500 on: SwitchOn::RandCase(stmt),
3501 arms,
3502 default_blk: join,
3503 report: None,
3504 },
3505 );
3506 Ok(join)
3507 }
3508
3509 /// `wait_order(e1, ..., en) pass else fail;` (LRM 1800-2023 §15.5.4):
3510 /// step `i` waits on `e_i` .. `e_n`, and goes on only if `e_i` is the one
3511 /// that fired (the wait's term 0, [`SwitchOn::FiredTerm`]) -- any other
3512 /// fires out of order and fails. After `e_n`, the pass action; a
3513 /// failure without an `else` reports `$error`.
3514 fn walk_wait_order(
3515 &mut self,
3516 stmt: Statement<'d>,
3517 cur: BlockId,
3518 ) -> Result<BlockId, LoweringError> {
3519 let events = stmt.wait_order_events();
3520 if events.is_empty() {
3521 return Err(LoweringError::malformed("wait_order with no events"));
3522 }
3523 let pass = self.alloc_block();
3524 let fail = self.alloc_block();
3525 let join = self.alloc_block();
3526 let if_false = stmt.wait_order_if_false();
3527 let report = if_false.is_none().then(|| stmt.source_range().start);
3528 let mut at = cur;
3529 for i in 0..events.len() {
3530 let resume = self.alloc_block();
3531 self.close(
3532 at,
3533 Terminator::Suspend {
3534 point: SuspendPoint::EdgeList(
3535 events[i..]
3536 .iter()
3537 .map(|e| (*e, sv_lang::EdgeKind::None))
3538 .collect(),
3539 vec![Vec::new(); events.len() - i],
3540 ),
3541 resume_blk: resume,
3542 },
3543 );
3544 let next = if i + 1 == events.len() {
3545 pass
3546 } else {
3547 self.alloc_block()
3548 };
3549 self.close(
3550 resume,
3551 Terminator::Switch {
3552 on: SwitchOn::FiredTerm,
3553 arms: vec![next],
3554 default_blk: fail,
3555 report,
3556 },
3557 );
3558 at = next;
3559 }
3560 let pass_exit = match stmt.wait_order_if_true() {
3561 Some(s) => self.walk_stmt(s, pass)?,
3562 None => pass,
3563 };
3564 self.close(pass_exit, Terminator::Fallthrough(join));
3565 let fail_exit = match if_false {
3566 Some(s) => self.walk_stmt(s, fail)?,
3567 None => fail,
3568 };
3569 self.close(fail_exit, Terminator::Fallthrough(join));
3570 Ok(join)
3571 }
3572
3573 /// Inlines `call` (to `task`, whose body can suspend) at `cur`: a fresh
3574 /// entry block binds the formals ([`ArgBinding::In`]/[`ArgBinding::
3575 /// Init`]) and initializes the task's locals, the body is walked in
3576 /// place (a `return;` jumps to the continuation), and the continuation
3577 /// block starts with the output copy-backs ([`ArgBinding::Out`]).
3578 fn inline_task_call(
3579 &mut self,
3580 call: Expression<'d>,
3581 task: Symbol<'d>,
3582 cur: BlockId,
3583 ) -> Result<BlockId, LoweringError> {
3584 let result_temp = self.pending_result_temp.take();
3585 // Through a virtual interface (`vif.t(...)`): the task of the
3586 // instance it selects -- one inlined copy per candidate instance
3587 // (`netlist::vif`), picked by its id; `null` runs nothing.
3588 if let Some((vif, targets)) = crate::netlist::vif::call_targets(&call) {
3589 let actuals: Vec<Expression<'d>> = call.operands()[1..].to_vec();
3590 let join = self.alloc_block();
3591 let mut arms = Vec::new();
3592 for target in targets {
3593 let arm = self.alloc_block();
3594 arms.push(arm);
3595 match target {
3596 Some(t) => {
3597 let exit = self.inline_task_body(&actuals, t, arm, None)?;
3598 self.park_call_result(exit, result_temp, t);
3599 self.close(exit, Terminator::Fallthrough(join));
3600 }
3601 None => self.close(arm, Terminator::Fallthrough(join)),
3602 }
3603 }
3604 self.close(
3605 cur,
3606 Terminator::Switch {
3607 on: SwitchOn::VifId(vif),
3608 arms,
3609 default_blk: join,
3610 report: None,
3611 },
3612 );
3613 return Ok(join);
3614 }
3615 // A class-method task (LRM 8.6): its `this` is bound to the receiver
3616 // (`obj.run()`), or for an implicit-`this` call (`run()` inside
3617 // another method) to the caller's current object, and kept in a frame
3618 // local across the suspend points.
3619 self.call_anchor = Some(call);
3620 let receiver = call.call_this_class();
3621 let mut actuals = call.operands();
3622 if receiver.is_some() && !actuals.is_empty() {
3623 actuals.remove(0);
3624 }
3625 // A `pure virtual` task has no body and so no `this` of its own; its
3626 // calls still dispatch over the implementations below (LRM 8.21).
3627 let pure = task.subroutine_flags().contains(sv_lang::MethodFlags::PURE);
3628 let this_var = task.subroutine_this_var();
3629 if this_var.is_none() && !pure {
3630 let exit = self.inline_task_body(&actuals, task, cur, None)?;
3631 self.park_call_result(exit, result_temp, task);
3632 return Ok(exit);
3633 }
3634 // A virtual task is bound by the object's class at run time (LRM
3635 // 8.20): one inlined override per implementation the possible dynamic
3636 // classes resolve it to, picked by the object's class. A call through
3637 // `super.` is not virtual.
3638 if !(receiver.is_none() && is_explicit_super(call))
3639 && (task
3640 .subroutine_flags()
3641 .contains(sv_lang::MethodFlags::VIRTUAL)
3642 || task.subroutine_override().is_some())
3643 {
3644 let impls = self
3645 .virtual_tasks
3646 .as_ref()
3647 .map(|v| v.implementations(&task))
3648 .unwrap_or_default();
3649 // No class the design creates implements it: no object can
3650 // reach the call, so it can only run on a null handle (an
3651 // error at run time, as for any call through `null`). E.g. UVM's
3652 // `uvm_sequencer_base::start_phase_sequence` in a bench with no
3653 // sequencer.
3654 let join = self.alloc_block();
3655 let mut arms = Vec::new();
3656 let mut ids = Vec::new();
3657 for (class_ids, imp) in impls {
3658 let this_var = imp.subroutine_this_var().ok_or_else(|| {
3659 LoweringError::malformed(format!(
3660 "override of virtual task `{}` has no `this`",
3661 task.name()
3662 ))
3663 })?;
3664 let arm = self.alloc_block();
3665 arms.push(arm);
3666 ids.push(class_ids);
3667 let exit = self.inline_task_body(&actuals, imp, arm, Some((this_var, receiver)))?;
3668 self.park_call_result(exit, result_temp, imp);
3669 self.close(exit, Terminator::Fallthrough(join));
3670 }
3671 self.close(
3672 cur,
3673 Terminator::Switch {
3674 on: SwitchOn::ObjClass { receiver, ids },
3675 arms,
3676 default_blk: join,
3677 // A null (or dead) handle runs no override: an error.
3678 report: Some(call.source_range().start),
3679 },
3680 );
3681 return Ok(join);
3682 }
3683 let this_var = this_var.ok_or_else(|| {
3684 LoweringError::malformed(format!("method task `{}` has no `this`", task.name()))
3685 })?;
3686 let exit = self.inline_task_body(&actuals, task, cur, Some((this_var, receiver)))?;
3687 self.park_call_result(exit, result_temp, task);
3688 Ok(exit)
3689 }
3690
3691 /// At the end of an inlined function `imp` (block `exit`): copies its
3692 /// result variable into the call's capture temporary, when it has one.
3693 fn park_call_result(
3694 &mut self,
3695 exit: BlockId,
3696 result_temp: Option<(u32, Expression<'d>)>,
3697 imp: Symbol<'d>,
3698 ) {
3699 if let Some((temp, _)) = result_temp
3700 && let Some(rv) = imp.subroutine_return_val_var()
3701 {
3702 self.blocks[exit.0 as usize]
3703 .bindings
3704 .push(ArgBinding::ToCaptured { temp, formal: rv });
3705 }
3706 }
3707
3708 /// The first call of a task that calls itself (directly or through other
3709 /// tasks) and contains timing controls (LRM 1800-2023 §13.4, §13.4.1):
3710 /// inlining cannot bound the depth, so each activation is a forked child
3711 /// of its caller that the caller joins. The caller evaluates the
3712 /// arguments now ([`ArgBinding::Capture`]), the child copies them into
3713 /// its own formals ([`ArgBinding::FromCaptured`]) and runs the body in a
3714 /// frame of its own (automatic variables, §6.21); a call inside the body
3715 /// forks the same kernel again ([`Self::recursive_self_call`]).
3716 ///
3717 /// `output` and `inout` formals (LRM 1800-2023 13.5) are copied out when
3718 /// the activation ends: parked in a temporary ([`ArgBinding::ToCaptured`]),
3719 /// handed to the spawner's own temporary ([`InheritedSlot::Return`]) and
3720 /// assigned to the actual after the join ([`ArgBinding::AssignCaptured`]).
3721 /// They are limited to what a capture temporary holds (a packed value of
3722 /// at most 128 bits).
3723 ///
3724 /// A class method (LRM 1800-2023 8.11) also takes its `this` as capture 0
3725 /// (an implicit receiver by [`ArgBinding::CaptureThis`]). A recursive
3726 /// void function that spawns processes is handled the same way; its
3727 /// `join_none` children may then start earlier than in a call that never
3728 /// yields (the caller waits at each recursive join).
3729 ///
3730 /// Deviation: the activation is a fork child, so `disable fork` and
3731 /// `wait fork` of the caller see it; only tasks with automatic lifetime
3732 /// are supported (`ref` / static formals and a `disable` of the task fail
3733 /// by name).
3734 fn inline_recursive_task(
3735 &mut self,
3736 actuals: &[Expression<'d>],
3737 task: Symbol<'d>,
3738 cur: BlockId,
3739 this: Option<(Symbol<'d>, Option<Expression<'d>>)>,
3740 ) -> Result<BlockId, LoweringError> {
3741 let refuse = |why: &str| {
3742 LoweringError::unsupported(format!(
3743 "recursive call to task `{}`, which contains timing controls: {why} \
3744 (LRM 1800-2023 13.4.1)",
3745 task.name()
3746 ))
3747 };
3748 let body = proc_body(&task).ok_or_else(|| {
3749 LoweringError::malformed(format!("task `{}` has no body", task.name()))
3750 })?;
3751 let mut formals: Vec<Symbol<'d>> = task.subroutine_arguments().collect();
3752 if formals.len() != actuals.len() {
3753 return Err(LoweringError::malformed(format!(
3754 "call to task `{}` has {} arguments for {} formals",
3755 task.name(),
3756 actuals.len(),
3757 formals.len()
3758 )));
3759 }
3760 if task.subroutine_kind() == Some(sv_lang::SubroutineKind::Function)
3761 && task.subroutine_return_type().is_some_and(|t| !t.is_void())
3762 {
3763 return Err(refuse("a value-returning function is not supported"));
3764 }
3765 for f in &formals {
3766 if crate::netlist::is_static_procedural_var(f) {
3767 return Err(refuse("a recursive task must be `automatic`"));
3768 }
3769 }
3770 // slang binds an output/inout actual as `actual = <formal>`: the
3771 // actual is its target.
3772 let mut actuals: Vec<Expression<'d>> = actuals
3773 .iter()
3774 .zip(&formals)
3775 .map(|(&a, f)| {
3776 if f.formal_argument_direction() == sv_lang::ArgumentDirection::In {
3777 a
3778 } else {
3779 call_actual_lvalue(a)
3780 }
3781 })
3782 .collect();
3783 // A class method: its `this` is formal 0, an input handle.
3784 let has_this = this.is_some();
3785 let mut implicit_this = false;
3786 if let Some((this_var, recv)) = this {
3787 let anchor = self.call_anchor.ok_or_else(|| {
3788 LoweringError::malformed("recursive method call without a call expression")
3789 })?;
3790 implicit_this = recv.is_none();
3791 formals.insert(0, this_var);
3792 actuals.insert(0, recv.unwrap_or(anchor));
3793 }
3794 let n = formals.len() as u32;
3795 let actuals = &actuals[..];
3796 // A `ref` formal bound to static storage (a variable or a net, directly or
3797 // through an enclosing call's aliased `ref`) is that storage in every
3798 // activation (LRM 1800-2023 13.5.2): no capture, no copy-back. Every
3799 // recursive call must bind it to the same storage
3800 // ([`Self::recursive_self_call`]); anything else is refused by name.
3801 let mut ref_alias: Vec<Option<Symbol<'d>>> = vec![None; formals.len()];
3802 for (i, f) in formals.iter().enumerate() {
3803 if f.formal_argument_direction() != sv_lang::ArgumentDirection::Ref {
3804 continue;
3805 }
3806 let is_event = f
3807 .value_type()
3808 .as_ref()
3809 .is_some_and(|t| crate::state::event_pool::event_count(t).is_some());
3810 match self.ref_storage(actuals[i]) {
3811 Some(t) if !is_event => ref_alias[i] = Some(t),
3812 _ => {
3813 return Err(refuse(
3814 "a `ref` argument must name a static variable or net (or an \
3815 enclosing task's aliased `ref` formal)",
3816 ));
3817 }
3818 }
3819 }
3820 let is_in = |i: usize| {
3821 (has_this && i == 0)
3822 || formals[i].formal_argument_direction() == sv_lang::ArgumentDirection::In
3823 || ref_alias[i].is_some()
3824 };
3825 let outs: Vec<usize> = (0..formals.len()).filter(|&i| !is_in(i)).collect();
3826 let groups = if outs.is_empty() { 2 } else { 4 };
3827
3828 // The branch: its own builder. Capture temporaries `0..n` hold the
3829 // incoming arguments, `n..2n` those of a recursive call; with
3830 // `output`/`inout` formals `2n..3n` hold this activation's results
3831 // and `3n..4n` those of a recursive call.
3832 // The same task activated from the same context builds the same
3833 // Root: build it once per process ([`RootKey`]) and the lowering
3834 // compiles it once ([`ProcessCfg::share_id`]); only the call site's
3835 // bindings below are per site. Not under a clock inference.
3836 let key = (self.global_clock.is_none() && self.checker_clock.is_none()).then(|| {
3837 MemoKey::Root(RootKey {
3838 task: task.uid(),
3839 this_var: this.map(|(t, _)| t.uid()),
3840 default_clocking: self.default_clocking.map(|c| c.uid()),
3841 rec_outer: self.rec_outer.clone(),
3842 chain: self
3843 .rec_up
3844 .iter()
3845 .chain(self.rec.iter())
3846 .map(|r| r.share)
3847 .collect(),
3848 ref_aliases: self
3849 .ref_stack
3850 .iter()
3851 .map(|(f, t)| (f.uid(), t.uid()))
3852 .chain(
3853 formals
3854 .iter()
3855 .zip(&ref_alias)
3856 .filter_map(|(f, t)| t.map(|t| (f.uid(), t.uid()))),
3857 )
3858 .collect(),
3859 labels: self.fork_branch_names.clone(),
3860 ancestors: self.ancestor_blocks.clone(),
3861 })
3862 });
3863 let cached = key
3864 .as_ref()
3865 .and_then(|k| self.fork_memo.borrow().get(k).cloned());
3866 let mut branch = if let Some(hit) = cached {
3867 hit
3868 } else {
3869 let share = NEXT_SHARE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3870 let mut b = CfgBuilder::new();
3871 b.fork_memo = self.fork_memo.clone();
3872 b.global_clock = self.global_clock;
3873 b.virtual_tasks = self.virtual_tasks.clone();
3874 b.fork_branch_names = self.fork_branch_names.clone();
3875 b.ancestor_blocks = self.ancestor_blocks.clone();
3876 b.default_clocking = self.default_clocking;
3877 b.checker_clock = self.checker_clock;
3878 b.rec_outer = self.rec_outer.clone();
3879 b.rec_outer.push(task.uid());
3880 b.rec_up = self.rec_up.clone();
3881 b.rec_up.extend(self.rec.clone());
3882 b.rec = Some(RecursiveTask {
3883 task: task.uid(),
3884 share,
3885 proto: actuals
3886 .iter()
3887 .copied()
3888 .zip(formals.iter().copied())
3889 .collect(),
3890 n,
3891 groups,
3892 outs: (0..formals.len()).map(|i| !is_in(i)).collect(),
3893 refs: ref_alias.clone(),
3894 has_this,
3895 body,
3896 });
3897 b.ref_stack = self.ref_stack.clone();
3898 for (f, t) in formals.iter().zip(&ref_alias) {
3899 if let Some(t) = t {
3900 b.ref_stack.push((*f, *t));
3901 b.ref_candidates.push((*f, Some(*t)));
3902 }
3903 }
3904 if let Some((this_var, _)) = this {
3905 b.this_stack.push(this_var);
3906 }
3907 for _ in 0..groups {
3908 for (f, &a) in formals.iter().zip(actuals) {
3909 b.set_capture_type(a, *f);
3910 }
3911 }
3912 let entry = b.alloc_block();
3913 for (i, f) in formals.iter().enumerate() {
3914 // An `output` formal starts out as its type's default.
3915 if ref_alias[i].is_some() {
3916 continue;
3917 }
3918 b.blocks[entry.0 as usize].bindings.push(
3919 if !is_in(i) && f.formal_argument_direction() == sv_lang::ArgumentDirection::Out
3920 {
3921 ArgBinding::Init { var: *f }
3922 } else {
3923 ArgBinding::FromCaptured {
3924 temp: i as u32,
3925 formal: *f,
3926 }
3927 },
3928 );
3929 b.inlined_locals.push(*f);
3930 }
3931 for local in task_locals(&task) {
3932 // A method's `this` is bound above (formal 0).
3933 if this.is_some_and(|(t, _)| t.uid() == local.uid()) {
3934 continue;
3935 }
3936 if crate::netlist::is_static_procedural_var(&local) {
3937 continue;
3938 }
3939 if !is_automatic_block_var(&local) {
3940 b.blocks[entry.0 as usize]
3941 .bindings
3942 .push(ArgBinding::Init { var: local });
3943 }
3944 b.inlined_locals.push(local);
3945 }
3946 for v in crate::patterns::pattern_vars(body.as_sem_node()) {
3947 if !b.inlined_locals.iter().any(|l| l.uid() == v.uid()) {
3948 b.inlined_locals.push(v);
3949 }
3950 }
3951 let after = b.alloc_block();
3952 b.target_depth.insert(after, 0);
3953 b.return_targets.push(after);
3954 // `disable task;` ends every activation (LRM 9.6.2): each one's exit
3955 // is the end of its own kernel.
3956 let task_name = task.name().to_string();
3957 let task_scoped = is_disabled_somewhere(&task_name);
3958 if task_scoped {
3959 b.scope_chain
3960 .push((task_name.clone(), after, b.frame_stack.len()));
3961 b.disable_targets
3962 .push((task_name, after, b.frame_stack.len()));
3963 }
3964 // LRM 19.3 / 19.5.5: every activation signals the task's `begin` at
3965 // its first statement and `end` at its exit block (also reached by a
3966 // `disable task;`), like an inlined call.
3967 b.block_event(entry, task, false);
3968 b.event_stack.push((task, b.disable_targets.len()));
3969 let exit = b.walk_stmt(body, entry);
3970 b.event_stack.pop();
3971 if task_scoped {
3972 b.scope_chain.pop();
3973 b.disable_targets.pop();
3974 }
3975 let exit = exit?;
3976 b.close(exit, Terminator::Fallthrough(after));
3977 b.block_event(after, task, true);
3978 for &i in &outs {
3979 b.blocks[after.0 as usize]
3980 .bindings
3981 .push(ArgBinding::ToCaptured {
3982 temp: 2 * n + i as u32,
3983 formal: formals[i],
3984 });
3985 }
3986 b.close(after, Terminator::End { loop_back: None });
3987 let inlined_locals = std::mem::take(&mut b.inlined_locals);
3988 let captures = std::mem::take(&mut b.captures);
3989 let capture_types = std::mem::take(&mut b.capture_types);
3990 let b_ref_formals: Vec<Symbol<'d>> = b.ref_stack.iter().map(|&(f, _)| f).collect();
3991 let ref_bindings = b.take_ref_bindings();
3992 let disable_scopes = b.take_disable_scopes();
3993 let disable_broadcast = std::mem::take(&mut b.disable_broadcast);
3994 let fork_branch_names = std::mem::take(&mut b.fork_branch_names);
3995 let mut built = ProcessCfg {
3996 blocks: b.finish(),
3997 entry,
3998 body,
3999 inlined_locals,
4000 captures,
4001 captured: Vec::new(),
4002 ref_bindings,
4003 disable_names: Vec::new(),
4004 fork_branch_names,
4005 disable_scopes,
4006 disable_broadcast,
4007 inherited: Vec::new(),
4008 capture_types,
4009 recursion: Recursion::Root,
4010 share_id: None,
4011 };
4012 built.captured = captured_vars(&built);
4013 built
4014 .captured
4015 .retain(|c| !b_ref_formals.iter().any(|f| f.uid() == c.uid()));
4016 if let Some(k) = key {
4017 built.share_id = Some(share);
4018 self.fork_memo.borrow_mut().insert(k, built.clone());
4019 }
4020 built
4021 };
4022
4023 // The caller: the arguments, evaluated now, into temporaries the
4024 // child inherits by value.
4025 let call = self.alloc_block();
4026 self.close(cur, Terminator::Fallthrough(call));
4027 for (i, (f, &a)) in formals.iter().zip(actuals).enumerate() {
4028 let temp = self.set_capture_type(a, *f);
4029 self.blocks[call.0 as usize]
4030 .bindings
4031 .push(if i == 0 && implicit_this {
4032 ArgBinding::CaptureThis { temp }
4033 } else {
4034 ArgBinding::Capture { temp, value: a }
4035 });
4036 branch.inherited.push(InheritedSlot::Temp {
4037 parent: temp,
4038 child: i as u32,
4039 });
4040 }
4041 let join_blk = self.alloc_block();
4042 // The results, back into the actuals once the activation is done.
4043 for &i in &outs {
4044 let ret = self.set_capture_type(actuals[i], formals[i]);
4045 branch.inherited.push(InheritedSlot::Return {
4046 parent: ret,
4047 child: 2 * n + i as u32,
4048 });
4049 self.blocks[join_blk.0 as usize]
4050 .bindings
4051 .push(ArgBinding::AssignCaptured {
4052 temp: ret,
4053 target: actuals[i],
4054 nba: false,
4055 });
4056 }
4057 self.close(
4058 call,
4059 Terminator::ForkJoin {
4060 kind: StatementBlockKind::JoinAll,
4061 branches: vec![branch],
4062 join_blk,
4063 },
4064 );
4065 Ok(join_blk)
4066 }
4067
4068 /// A call of the recursive task inside its own activation
4069 /// ([`Self::inline_recursive_task`]): the arguments go to capture
4070 /// temporaries `n..2n`, and a placeholder branch forks the activation's
4071 /// own kernel again ([`Recursion::SelfSpawn`]), joined at once.
4072 fn recursive_self_call(
4073 &mut self,
4074 actuals: &[Expression<'d>],
4075 cur: BlockId,
4076 recv: Option<Option<Expression<'d>>>,
4077 ) -> Result<BlockId, LoweringError> {
4078 let rec = self
4079 .rec
4080 .as_ref()
4081 .ok_or_else(|| LoweringError::malformed("recursive call outside its activation"))?;
4082 let (n, body, rec_outs) = (rec.n, rec.body, rec.outs.clone());
4083 let skip = usize::from(rec.has_this);
4084 for (a, t) in actuals.iter().zip(&rec.refs[skip..]) {
4085 if let Some(t) = t
4086 && self.ref_storage(call_actual_lvalue(*a)).map(|s| s.uid()) != Some(t.uid())
4087 {
4088 return Err(LoweringError::unsupported(
4089 "recursive call whose `ref` argument is not the storage the first call \
4090 bound (a recursive task's `ref` formal is an alias of one static \
4091 variable or net; LRM 1800-2023 13.5.2)",
4092 ));
4093 }
4094 }
4095 let mut actuals: Vec<Expression<'d>> = actuals
4096 .iter()
4097 .zip(&rec_outs[skip..])
4098 .map(|(&a, &out)| if out { call_actual_lvalue(a) } else { a })
4099 .collect();
4100 // The receiver of a method call is capture 0.
4101 let mut implicit_this = false;
4102 if let Some(r) = recv {
4103 let anchor = self.call_anchor.ok_or_else(|| {
4104 LoweringError::malformed("recursive method call without a call expression")
4105 })?;
4106 implicit_this = r.is_none();
4107 actuals.insert(0, r.unwrap_or(anchor));
4108 }
4109 let actuals = &actuals[..];
4110 if actuals.len() as u32 != n {
4111 return Err(LoweringError::malformed(
4112 "recursive call with the wrong number of arguments",
4113 ));
4114 }
4115 let call = self.alloc_block();
4116 self.close(cur, Terminator::Fallthrough(call));
4117 let mut inherited = Vec::new();
4118 for (i, &a) in actuals.iter().enumerate() {
4119 let temp = n + i as u32;
4120 self.blocks[call.0 as usize]
4121 .bindings
4122 .push(if i == 0 && implicit_this {
4123 ArgBinding::CaptureThis { temp }
4124 } else {
4125 ArgBinding::Capture { temp, value: a }
4126 });
4127 inherited.push(InheritedSlot::Temp {
4128 parent: temp,
4129 child: i as u32,
4130 });
4131 }
4132 // The results of the callee, back into the actuals after the join.
4133 let join_blk = self.alloc_block();
4134 for (i, &a) in actuals.iter().enumerate() {
4135 if rec_outs[i] {
4136 let ret = 3 * n + i as u32;
4137 inherited.push(InheritedSlot::Return {
4138 parent: ret,
4139 child: 2 * n + i as u32,
4140 });
4141 self.blocks[join_blk.0 as usize]
4142 .bindings
4143 .push(ArgBinding::AssignCaptured {
4144 temp: ret,
4145 target: a,
4146 nba: false,
4147 });
4148 }
4149 }
4150 let mut stub = CfgBuilder::new();
4151 let entry = stub.alloc_block();
4152 stub.close(entry, Terminator::End { loop_back: None });
4153 let branch = ProcessCfg {
4154 blocks: stub.finish(),
4155 entry,
4156 body,
4157 inlined_locals: Vec::new(),
4158 captures: Vec::new(),
4159 captured: Vec::new(),
4160 ref_bindings: Vec::new(),
4161 disable_names: Vec::new(),
4162 fork_branch_names: Vec::new(),
4163 disable_scopes: Vec::new(),
4164 disable_broadcast: Vec::new(),
4165 inherited,
4166 capture_types: Vec::new(),
4167 recursion: Recursion::SelfSpawn,
4168 share_id: None,
4169 };
4170 self.close(
4171 call,
4172 Terminator::ForkJoin {
4173 kind: StatementBlockKind::JoinAll,
4174 branches: vec![branch],
4175 join_blk,
4176 },
4177 );
4178 Ok(join_blk)
4179 }
4180
4181 /// A call, inside one activation, of the task of the enclosing activation
4182 /// `self.rec_up[k]` (mutual recursion, LRM 1800-2023 13.4.1): the
4183 /// arguments go to fresh capture temporaries and a placeholder branch
4184 /// forks that activation's kernel again ([`Recursion::AncestorSpawn`]),
4185 /// joined at once.
4186 fn recursive_ancestor_call(
4187 &mut self,
4188 k: usize,
4189 actuals: &[Expression<'d>],
4190 cur: BlockId,
4191 recv: Option<Option<Expression<'d>>>,
4192 ) -> Result<BlockId, LoweringError> {
4193 let anc = self.rec_up[k].clone();
4194 if anc.refs.iter().any(Option::is_some) {
4195 return Err(LoweringError::unsupported(
4196 "mutually recursive tasks with a `ref` formal (LRM 1800-2023 13.4.1, 13.5.2)",
4197 ));
4198 }
4199 let skip = usize::from(anc.has_this);
4200 let mut actuals: Vec<Expression<'d>> = actuals
4201 .iter()
4202 .zip(&anc.outs[skip..])
4203 .map(|(&a, &out)| if out { call_actual_lvalue(a) } else { a })
4204 .collect();
4205 let mut implicit_this = false;
4206 if let Some(r) = recv {
4207 let anchor = self.call_anchor.ok_or_else(|| {
4208 LoweringError::malformed("recursive method call without a call expression")
4209 })?;
4210 implicit_this = r.is_none();
4211 actuals.insert(0, r.unwrap_or(anchor));
4212 }
4213 if actuals.len() as u32 != anc.n {
4214 return Err(LoweringError::malformed(
4215 "recursive call with the wrong number of arguments",
4216 ));
4217 }
4218 let call = self.alloc_block();
4219 self.close(cur, Terminator::Fallthrough(call));
4220 let mut inherited = Vec::new();
4221 for (i, &a) in actuals.iter().enumerate() {
4222 let temp = self.set_capture_type(a, anc.proto[i].1);
4223 self.blocks[call.0 as usize]
4224 .bindings
4225 .push(if i == 0 && implicit_this {
4226 ArgBinding::CaptureThis { temp }
4227 } else {
4228 ArgBinding::Capture { temp, value: a }
4229 });
4230 inherited.push(InheritedSlot::Temp {
4231 parent: temp,
4232 child: i as u32,
4233 });
4234 }
4235 // The ancestor's results, back into the actuals after the join.
4236 let join_blk = self.alloc_block();
4237 for (i, &a) in actuals.iter().enumerate() {
4238 if anc.outs[i] {
4239 let ret = self.set_capture_type(a, anc.proto[i].1);
4240 inherited.push(InheritedSlot::Return {
4241 parent: ret,
4242 child: 2 * anc.n + i as u32,
4243 });
4244 self.blocks[join_blk.0 as usize]
4245 .bindings
4246 .push(ArgBinding::AssignCaptured {
4247 temp: ret,
4248 target: a,
4249 nba: false,
4250 });
4251 }
4252 }
4253 let mut stub = CfgBuilder::new();
4254 let entry = stub.alloc_block();
4255 stub.close(entry, Terminator::End { loop_back: None });
4256 let branch = ProcessCfg {
4257 blocks: stub.finish(),
4258 entry,
4259 body: anc.body,
4260 inlined_locals: Vec::new(),
4261 captures: Vec::new(),
4262 captured: Vec::new(),
4263 ref_bindings: Vec::new(),
4264 disable_names: Vec::new(),
4265 fork_branch_names: Vec::new(),
4266 disable_scopes: Vec::new(),
4267 disable_broadcast: Vec::new(),
4268 inherited,
4269 capture_types: Vec::new(),
4270 recursion: Recursion::AncestorSpawn((self.rec_up.len() - k) as u32),
4271 share_id: None,
4272 };
4273 self.close(
4274 call,
4275 Terminator::ForkJoin {
4276 kind: StatementBlockKind::JoinAll,
4277 branches: vec![branch],
4278 join_blk,
4279 },
4280 );
4281 Ok(join_blk)
4282 }
4283
4284 /// A new capture temporary shaped like `formal`, holding `value`.
4285 fn set_capture_type(&mut self, value: Expression<'d>, formal: Symbol<'d>) -> u32 {
4286 let temp = self.captures.len() as u32;
4287 self.captures.push(value);
4288 self.capture_types.resize(temp as usize, None);
4289 self.capture_types.push(Some(formal));
4290 temp
4291 }
4292
4293 /// [`Self::inline_task_call`] of `task` with the arguments `actuals`.
4294 /// An expression statement that calls a value-returning function whose
4295 /// body spawns processes (`fork ... join_none` in a function, LRM
4296 /// 1800-2023 13.4.4), which cannot be compiled: each such call is inlined
4297 /// ahead of the statement (arguments first, in source order) and the
4298 /// statement reads the function's result variable instead of calling it
4299 /// (codegen `lower_user_subroutine_call_of`). `None` when the statement
4300 /// has no such call. The same function twice in one statement, or a call
4301 /// to a virtual method, is refused by name.
4302 fn hoist_suspending_function_calls(
4303 &mut self,
4304 stmt: Statement<'d>,
4305 cur: BlockId,
4306 ) -> Result<Option<BlockId>, LoweringError> {
4307 let Some(cur) = self.hoist_suspending_calls_in(stmt.as_sem_node(), cur)? else {
4308 return Ok(None);
4309 };
4310 self.push(cur, stmt);
4311 Ok(Some(cur))
4312 }
4313
4314 /// [`Self::hoist_suspending_function_calls`]' inlining half over any
4315 /// expression tree: inlines every spawning function call under `node`
4316 /// ahead of the caller, which then reads their result variables. Also
4317 /// used for an inlined function's `return expr;` (`return m(...)`).
4318 fn hoist_suspending_calls_in(
4319 &mut self,
4320 node: SemNode<'d>,
4321 cur: BlockId,
4322 ) -> Result<Option<BlockId>, LoweringError> {
4323 let mut calls = Vec::new();
4324 collect_suspending_function_calls(node, &mut calls);
4325 if calls.is_empty() {
4326 return Ok(None);
4327 }
4328 let mut count: std::collections::HashMap<_, usize> = std::collections::HashMap::new();
4329 for call in &calls {
4330 if let Some(callee) = user_task_callee(call) {
4331 *count.entry(callee.uid()).or_default() += 1;
4332 }
4333 }
4334 let mut cur = cur;
4335 for call in calls {
4336 let Some(callee) = user_task_callee(&call) else {
4337 continue;
4338 };
4339 // A method (LRM 8.6) is inlined with its `this` bound to the
4340 // receiver (`inline_task_call`). A virtual one picks among
4341 // several overrides, each with a result variable of its own.
4342 let is_method =
4343 callee.subroutine_this_var().is_some() || call.call_this_class().is_some();
4344 let flags = callee.subroutine_flags();
4345 let is_virtual = is_method
4346 && (flags.contains(sv_lang::MethodFlags::VIRTUAL)
4347 || flags.contains(sv_lang::MethodFlags::PURE)
4348 || callee.subroutine_override().is_some());
4349 // The function's result variable is shared by its calls (and a
4350 // virtual method's overrides have one each): a call that cannot
4351 // read it straight after its inlining parks the value in a
4352 // temporary of its own, which the call's expression reads.
4353 let temp = (is_virtual || count.get(&callee.uid()).copied().unwrap_or(0) > 1)
4354 .then(|| {
4355 callee
4356 .subroutine_return_val_var()
4357 .map(|rv| self.set_capture_type(call, rv))
4358 })
4359 .flatten();
4360 if let Some(temp) = temp {
4361 self.pending_result_temp = Some((temp, call));
4362 }
4363 cur = if is_method {
4364 self.inline_task_call(call, callee, cur)?
4365 } else {
4366 let actuals = call.operands();
4367 let exit = self.inline_task_body(&actuals, callee, cur, None)?;
4368 self.park_call_result(exit, temp.map(|t| (t, call)), callee);
4369 exit
4370 };
4371 self.pending_result_temp = None;
4372 if let Some(temp) = temp {
4373 self.blocks[cur.0 as usize]
4374 .bindings
4375 .push(ArgBinding::CallResult { temp, call });
4376 }
4377 }
4378 Ok(Some(cur))
4379 }
4380
4381 fn inline_task_body(
4382 &mut self,
4383 actuals: &[Expression<'d>],
4384 task: Symbol<'d>,
4385 cur: BlockId,
4386 this: Option<(Symbol<'d>, Option<Expression<'d>>)>,
4387 ) -> Result<BlockId, LoweringError> {
4388 if let Some(rec) = &self.rec
4389 && rec.task == task.uid()
4390 && this.is_some() == rec.has_this
4391 {
4392 return self.recursive_self_call(actuals, cur, this.map(|t| t.1));
4393 }
4394 if let Some(k) = self
4395 .rec_up
4396 .iter()
4397 .rposition(|r| r.task == task.uid() && r.has_this == this.is_some())
4398 {
4399 return self.recursive_ancestor_call(k, actuals, cur, this.map(|t| t.1));
4400 }
4401 if self.rec.is_none() && self.rec_outer.contains(&task.uid()) {
4402 // A fork branch the activation does not hand its recursion to
4403 // (a virtual-task or detached branch): inlining would not end.
4404 return Err(LoweringError::unsupported(format!(
4405 "call to task `{}` from a branch of its own recursive activation that cannot \
4406 spawn it (LRM 1800-2023 13.4.1)",
4407 task.name()
4408 )));
4409 }
4410 if self.inline_stack.contains(&task.uid()) {
4411 return Err(LoweringError::unsupported(format!(
4412 "recursive call to task `{}`, which contains timing controls (it has to be \
4413 inlined into the calling process)",
4414 task.name()
4415 )));
4416 }
4417 if !self.rec_outer.contains(&task.uid())
4418 && task_reaches_itself(&task, self.virtual_tasks.as_deref())
4419 {
4420 return self.inline_recursive_task(actuals, task, cur, this);
4421 }
4422 let body = proc_body(&task).ok_or_else(|| {
4423 LoweringError::malformed(format!("task `{}` has no body", task.name()))
4424 })?;
4425 let formals: Vec<Symbol<'d>> = task.subroutine_arguments().collect();
4426 if formals.len() != actuals.len() {
4427 return Err(LoweringError::malformed(format!(
4428 "call to task `{}` has {} arguments for {} formals",
4429 task.name(),
4430 actuals.len(),
4431 formals.len()
4432 )));
4433 }
4434 let entry = self.alloc_block();
4435 self.close(cur, Terminator::Fallthrough(entry));
4436 let after = self.alloc_block();
4437 self.target_depth.insert(after, self.owned_scopes.len());
4438 let frame_mark = self.inlined_locals.len();
4439 let mut releases = Vec::new();
4440 if let Some((this_var, receiver)) = this {
4441 let binding = match receiver {
4442 Some(r) => ArgBinding::In {
4443 formal: this_var,
4444 actual: r,
4445 },
4446 None => ArgBinding::ThisOf { var: this_var },
4447 };
4448 self.blocks[entry.0 as usize].bindings.push(binding);
4449 self.inlined_locals.push(this_var);
4450 // Both bindings hold one count on the object for the call.
4451 releases.push(ArgBinding::Release { var: this_var });
4452 }
4453 let mut copy_backs = Vec::new();
4454 let mut aliases = Vec::new();
4455 let mut bound_refs: Vec<(Symbol<'d>, Expression<'d>)> = Vec::new();
4456 for (formal, &actual) in formals.iter().zip(actuals) {
4457 // slang binds an output/inout actual as `actual = <formal>`.
4458 let lvalue = if actual.kind() == ExpressionKind::Assignment {
4459 actual.left().ok_or_else(|| {
4460 LoweringError::malformed("output argument with no target expression")
4461 })?
4462 } else {
4463 actual
4464 };
4465 // A `ref` naming static storage (directly or through an
4466 // enclosing call's aliased `ref`) is that storage in the body.
4467 // (An `event` (or event array) ref formal is a handle: copied in and back out, like
4468 // `inout`, so the body's `r = e` merges the actual's event.)
4469 if formal.formal_argument_direction() == sv_lang::ArgumentDirection::Ref
4470 && !formal
4471 .value_type()
4472 .as_ref()
4473 .is_some_and(|t| crate::state::event_pool::event_count(t).is_some())
4474 && let Some(target) = self.ref_storage(lvalue)
4475 {
4476 self.ref_candidates.push((*formal, Some(target)));
4477 aliases.push((*formal, target));
4478 continue;
4479 }
4480 // A `ref` to a queue / dynamic-array element of a packed or real
4481 // type (LRM 1800-2023 13.5.2): the formal is a live alias, every
4482 // access of it goes through a runtime reference
4483 // ([`ArgBinding::RefOpen`]); other element types keep the
4484 // copy-in/copy-out of `inout`.
4485 if formal.formal_argument_direction() == sv_lang::ArgumentDirection::Ref
4486 && lvalue.kind() == ExpressionKind::ElementSelect
4487 && formal
4488 .value_type()
4489 .is_some_and(|t| t.is_integral() || t.is_floating())
4490 && lvalue
4491 .select_value()
4492 .and_then(|b| b.expr_type())
4493 .is_some_and(|t| t.is_dynamically_sized_array() || t.is_associative_array())
4494 {
4495 // One reference-id slot per formal, shared by every call
4496 // site: the body reaches the reference through the formal.
4497 let prior =
4498 self.blocks
4499 .iter()
4500 .flat_map(|b| b.bindings.iter())
4501 .find_map(|b| match b {
4502 ArgBinding::RefOpen {
4503 temp, formal: f, ..
4504 } if f.uid() == formal.uid() => Some(*temp),
4505 _ => None,
4506 });
4507 let temp = prior.unwrap_or_else(|| {
4508 self.captures.push(lvalue);
4509 self.captures.len() as u32 - 1
4510 });
4511 self.blocks[entry.0 as usize]
4512 .bindings
4513 .push(ArgBinding::RefOpen {
4514 temp,
4515 formal: *formal,
4516 actual: lvalue,
4517 });
4518 copy_backs.push(ArgBinding::RefClose {
4519 temp,
4520 formal: *formal,
4521 actual: lvalue,
4522 });
4523 self.ref_candidates.push((*formal, None));
4524 if !crate::netlist::is_static_procedural_var(formal) {
4525 self.inlined_locals.push(*formal);
4526 }
4527 // 37.15 detail 3: the formal is a ref obj of the task's
4528 // frame, bound to the element the call named.
4529 bound_refs.push((*formal, lvalue));
4530 continue;
4531 }
4532 let binding = match formal.formal_argument_direction() {
4533 sv_lang::ArgumentDirection::In => ArgBinding::In {
4534 formal: *formal,
4535 actual,
4536 },
4537 sv_lang::ArgumentDirection::Out => ArgBinding::Init { var: *formal },
4538 sv_lang::ArgumentDirection::InOut | sv_lang::ArgumentDirection::Ref => {
4539 ArgBinding::In {
4540 formal: *formal,
4541 actual: lvalue,
4542 }
4543 }
4544 };
4545 self.blocks[entry.0 as usize].bindings.push(binding);
4546 if formal.formal_argument_direction() == sv_lang::ArgumentDirection::Ref {
4547 bound_refs.push((*formal, lvalue));
4548 let named = (crate::symid::is_value_name(lvalue.kind())
4549 && !formal
4550 .value_type()
4551 .as_ref()
4552 .is_some_and(|t| crate::state::event_pool::event_count(t).is_some()))
4553 .then(|| lvalue.referenced_symbol())
4554 .flatten();
4555 self.ref_candidates.push((*formal, named));
4556 }
4557 if formal.formal_argument_direction() != sv_lang::ArgumentDirection::In {
4558 copy_backs.push(ArgBinding::Out {
4559 actual: lvalue,
4560 formal: *formal,
4561 });
4562 }
4563 // A static task's formal is the instance's (a signal).
4564 if !crate::netlist::is_static_procedural_var(formal) {
4565 self.inlined_locals.push(*formal);
4566 if formal.formal_argument_direction() == sv_lang::ArgumentDirection::In {
4567 releases.push(ArgBinding::Release { var: *formal });
4568 }
4569 }
4570 }
4571 // A method's body (and its locals' initializers) runs against its
4572 // own `this`, in blocks of its own; the bindings above ran in the
4573 // caller's.
4574 if let Some((this_var, _)) = this {
4575 self.this_stack.push(this_var);
4576 }
4577 let n_aliases = aliases.len();
4578 self.ref_stack.extend(aliases);
4579 let body_entry = if this.is_some() || n_aliases > 0 {
4580 let b = self.alloc_block();
4581 self.close(entry, Terminator::Fallthrough(b));
4582 b
4583 } else {
4584 entry
4585 };
4586 for local in task_locals(&task) {
4587 // A method's `this` is bound above, never a default-initialized
4588 // local.
4589 if this.is_some_and(|(t, _)| t.uid() == local.uid()) {
4590 continue;
4591 }
4592 // A static variable is the instance's (a signal); a block's
4593 // automatic variables are initialized on entry to it.
4594 if crate::netlist::is_static_procedural_var(&local) {
4595 continue;
4596 }
4597 if !is_automatic_block_var(&local) {
4598 self.blocks[body_entry.0 as usize]
4599 .bindings
4600 .push(ArgBinding::Init { var: local });
4601 releases.push(ArgBinding::Release { var: local });
4602 }
4603 self.inlined_locals.push(local);
4604 }
4605 // An inlined value-returning function (a `fork` in its body cannot be
4606 // compiled, LRM 13.4.4): its result variable is a local of the
4607 // process, which `return expr;` / `f = expr;` assign and the hoisted
4608 // call's expression reads (`hoist_suspending_function_calls`).
4609 let ret_var = if task.subroutine_kind() == Some(sv_lang::SubroutineKind::Function)
4610 && task.subroutine_return_type().is_some_and(|t| !t.is_void())
4611 {
4612 task.subroutine_return_val_var()
4613 } else {
4614 None
4615 };
4616 if let Some(rv) = ret_var {
4617 self.blocks[body_entry.0 as usize]
4618 .bindings
4619 .push(ArgBinding::Init { var: rv });
4620 self.inlined_locals.push(rv);
4621 }
4622 // The pattern variables of the body's `matches` (LRM 12.6).
4623 for v in crate::patterns::pattern_vars(body.as_sem_node()) {
4624 if !self.inlined_locals.iter().any(|l| l.uid() == v.uid()) {
4625 self.inlined_locals.push(v);
4626 }
4627 }
4628 // VPI frame (37.43): the formals and locals this inlining made
4629 // automatic variables of the process, in that order.
4630 let frame_vars: Vec<Symbol<'d>> = self.inlined_locals[frame_mark..]
4631 .iter()
4632 .copied()
4633 .filter(|v| this.is_none_or(|(t, _)| t.uid() != v.uid()))
4634 .filter(|v| !bound_refs.iter().any(|(f, _)| f.uid() == v.uid()))
4635 .collect();
4636 self.frame_probe(
4637 body_entry,
4638 FrameProbe::Enter {
4639 task,
4640 vars: frame_vars.clone(),
4641 bound: bound_refs,
4642 },
4643 );
4644 self.block_event(body_entry, task, false);
4645 self.event_stack.push((task, self.disable_targets.len()));
4646 self.frame_stack.push((task, frame_vars.clone()));
4647 self.inline_stack.push(task.uid());
4648 self.return_targets.push(after);
4649 self.ret_var_stack.push(ret_var);
4650 // `disable task;` ends this activation (LRM 9.6.2), from inside it or
4651 // from another process: the exit is the block after the call, whose
4652 // frame probe ends the activation's VPI frame.
4653 let task_name = task.name().to_string();
4654 let task_scoped = is_disabled_somewhere(&task_name);
4655 let mut n_tags = 0;
4656 if task_scoped {
4657 self.scope_chain
4658 .push((task_name.clone(), after, self.frame_stack.len()));
4659 self.disable_targets
4660 .push((task_name, after, self.frame_stack.len()));
4661 // LRM 25.7.4: an `extern` method's activation is also the scope
4662 // `disable m1.a.t;` (this module instance's call) and `disable
4663 // b.t;` (every call through the interface instance) name.
4664 for tag in crate::netlist::itf::extern_scope_tags(&task) {
4665 self.scope_chain.push((tag, after, self.frame_stack.len()));
4666 n_tags += 1;
4667 }
4668 }
4669 let exit = self.walk_stmt(body, body_entry);
4670 if task_scoped {
4671 self.scope_chain.pop();
4672 self.disable_targets.pop();
4673 for _ in 0..n_tags {
4674 self.scope_chain.pop();
4675 }
4676 }
4677 self.event_stack.pop();
4678 self.return_targets.pop();
4679 self.ret_var_stack.pop();
4680 self.inline_stack.pop();
4681 self.frame_stack.pop();
4682 if this.is_some() {
4683 self.this_stack.pop();
4684 }
4685 self.ref_stack.truncate(self.ref_stack.len() - n_aliases);
4686 let exit = exit?;
4687 self.close(exit, Terminator::Fallthrough(after));
4688 self.blocks[after.0 as usize].bindings.extend(copy_backs);
4689 self.blocks[after.0 as usize].bindings.extend(releases);
4690 self.block_event(after, task, true);
4691 self.frame_probe(
4692 after,
4693 FrameProbe::Exit {
4694 task,
4695 vars: frame_vars,
4696 },
4697 );
4698 Ok(after)
4699 }
4700
4701 /// An immediate assertion whose pass or fail statement waits (LRM 16.3):
4702 /// [`Terminator::Assertion`] over the two statements' own blocks.
4703 fn walk_immediate_assertion(
4704 &mut self,
4705 stmt: Statement<'d>,
4706 (pass_stmt, fail_stmt): (Option<Statement<'d>>, Option<Statement<'d>>),
4707 cur: BlockId,
4708 ) -> Result<BlockId, LoweringError> {
4709 if stmt.is_deferred_assertion() == Some(true) {
4710 return Err(LoweringError::unsupported(
4711 "a deferred assertion (LRM 16.4) whose action block waits (the action must be \
4712 a subroutine call that is queued)"
4713 .to_string(),
4714 ));
4715 }
4716 let pass_blk = self.alloc_block();
4717 let fail_blk = self.alloc_block();
4718 let done_blk = self.alloc_block();
4719 self.close(
4720 cur,
4721 Terminator::Assertion {
4722 stmt,
4723 pass_blk,
4724 fail_blk,
4725 done_blk,
4726 },
4727 );
4728 for (action, blk) in [(pass_stmt, pass_blk), (fail_stmt, fail_blk)] {
4729 let exit = match action {
4730 Some(a) => self.walk_stmt(a, blk)?,
4731 None => blk,
4732 };
4733 self.close(exit, Terminator::Fallthrough(done_blk));
4734 }
4735 Ok(done_blk)
4736 }
4737
4738 /// A `case` whose arms need their own blocks ([`Terminator::Case`]).
4739 fn walk_case(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
4740 // A `case ... matches` has its own item and default accessors.
4741 let is_pattern = stmt.kind() == StatementKind::PatternCase;
4742 // A constant selector over arms one of which slang demoted to
4743 // `Invalid` (a statically dead arm): only the matching arm can run
4744 // (LRM 12.5), so lower just it.
4745 if !is_pattern && let Some(live) = constant_case_live_arm(&stmt) {
4746 return match live {
4747 Some(arm) => self.walk_stmt(arm, cur),
4748 None => Ok(cur),
4749 };
4750 }
4751 let arm_stmts: Vec<Statement<'d>> = if is_pattern {
4752 stmt.pattern_case_items().iter().map(|i| i.stmt()).collect()
4753 } else {
4754 stmt.case_items().iter().map(|i| i.stmt()).collect()
4755 };
4756 let arms: Vec<BlockId> = arm_stmts.iter().map(|_| self.alloc_block()).collect();
4757 let default_blk = self.alloc_block();
4758 let join = self.alloc_block();
4759 self.close(
4760 cur,
4761 Terminator::Case {
4762 stmt,
4763 arms: arms.clone(),
4764 default_blk,
4765 },
4766 );
4767 for (arm_stmt, &arm) in arm_stmts.iter().zip(&arms) {
4768 let exit = self.walk_stmt(*arm_stmt, arm)?;
4769 self.close(exit, Terminator::Fallthrough(join));
4770 }
4771 let default_stmt = if is_pattern {
4772 stmt.pattern_case_default()
4773 } else {
4774 stmt.default_case()
4775 };
4776 let default_exit = match default_stmt {
4777 Some(d) => self.walk_stmt(d, default_blk)?,
4778 None => default_blk,
4779 };
4780 self.close(default_exit, Terminator::Fallthrough(join));
4781 Ok(join)
4782 }
4783
4784 /// Walks a loop body with `continue` going to `cont` and `break` to
4785 /// `brk`.
4786 fn walk_loop_body(
4787 &mut self,
4788 body_stmt: Statement<'d>,
4789 body: BlockId,
4790 cont: BlockId,
4791 brk: BlockId,
4792 ) -> Result<BlockId, LoweringError> {
4793 self.loop_targets.push((cont, brk));
4794 let open = self.owned_scopes.len();
4795 self.target_depth.insert(cont, open);
4796 self.target_depth.insert(brk, open);
4797 let exit = self.walk_stmt(body_stmt, body);
4798 self.loop_targets.pop();
4799 exit
4800 }
4801
4802 /// Walks a block, keeping [`Self::name_path`] current for the sequential
4803 /// block's own name.
4804 fn walk_block(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
4805 // `L: assert (..) ... disable L; ...`: one leaf (see the helper).
4806 if !self.entering_block && self_disabling_assertion_block(&stmt) {
4807 self.push(cur, stmt);
4808 return Ok(cur);
4809 }
4810 let name = (!self.entering_block
4811 && stmt.block_kind() == Some(StatementBlockKind::Sequential))
4812 .then(|| block_name(&stmt))
4813 .filter(|n| !n.is_empty());
4814 let pushed = name.is_some();
4815 if let Some(n) = name {
4816 self.name_path.push(n);
4817 }
4818 // LRM 19.3: `@@(begin blk)` / `@@(end blk)` of a named block, signalled
4819 // at its first statement and after its last (the codegen drops the
4820 // ones no covergroup names). Only a block that is walked into the CFG
4821 // gets here; a leaf block's events are emitted where it is lowered.
4822 let blk = (!self.entering_block
4823 && stmt.block_kind() == Some(StatementBlockKind::Sequential))
4824 .then(|| stmt.block_symbol())
4825 .flatten()
4826 .filter(|b| !b.name().is_empty());
4827 if let Some(b) = blk {
4828 self.block_event(cur, b, false);
4829 self.event_stack.push((b, self.disable_targets.len()));
4830 }
4831 let r = self.walk_block_inner(stmt, cur);
4832 if blk.is_some() {
4833 self.event_stack.pop();
4834 }
4835 if pushed {
4836 self.name_path.pop();
4837 }
4838 if let (Some(b), Ok(end)) = (blk, &r) {
4839 self.block_event(*end, b, true);
4840 }
4841 r
4842 }
4843
4844 fn walk_block_inner(
4845 &mut self,
4846 stmt: Statement<'d>,
4847 cur: BlockId,
4848 ) -> Result<BlockId, LoweringError> {
4849 // `StatementBlockKind` is a plain (non-`#[non_exhaustive]`) enum
4850 // hand-authored in `sv-lang`, so this literal `match` is genuinely,
4851 // compiler-checked exhaustive -- no wildcard needed or used.
4852 match stmt.block_kind() {
4853 // A named block some `disable` inside it can leave: its own exit.
4854 // A block declaring automatic variables: they are created and
4855 // initialized on each entry (LRM 1800-2023 §6.21), at the
4856 // start of a CFG block of its own.
4857 Some(_) if !self.entering_block && !block_automatic_vars(&stmt, true).is_empty() => {
4858 // A CFG block with no statements yet takes the bindings
4859 // itself (they run before its statements).
4860 let fresh = &self.blocks[cur.0 as usize];
4861 let entry = if fresh.stmts.is_empty() && fresh.step_exprs.is_empty() {
4862 cur
4863 } else {
4864 let entry = self.alloc_block();
4865 self.close(cur, Terminator::Fallthrough(entry));
4866 entry
4867 };
4868 for var in block_automatic_vars(&stmt, true) {
4869 self.blocks[entry.0 as usize]
4870 .bindings
4871 .push(ArgBinding::Init { var });
4872 }
4873 // A sequential block's handle-holding variables end with it.
4874 let owned: Vec<Symbol<'d>> =
4875 if stmt.block_kind() == Some(StatementBlockKind::Sequential) {
4876 block_automatic_vars(&stmt, true)
4877 .into_iter()
4878 .filter(|v| v.value_type().is_some_and(|t| may_hold_handles(&t)))
4879 .collect()
4880 } else {
4881 Vec::new()
4882 };
4883 let scoped = !owned.is_empty();
4884 if scoped {
4885 self.owned_scopes.push(owned.clone());
4886 }
4887 self.entering_block = true;
4888 let r = self.walk_block(stmt, entry);
4889 self.entering_block = false;
4890 if !scoped {
4891 return r;
4892 }
4893 self.owned_scopes.pop();
4894 let end = r?;
4895 let leave = self.alloc_block();
4896 self.close(end, Terminator::Fallthrough(leave));
4897 for var in owned.into_iter().rev() {
4898 self.blocks[leave.0 as usize]
4899 .bindings
4900 .push(ArgBinding::Release { var });
4901 }
4902 Ok(leave)
4903 }
4904 Some(StatementBlockKind::Sequential)
4905 if let name = block_name(&stmt)
4906 && !name.is_empty()
4907 && (is_disabled_somewhere(&name)
4908 || is_disabled_somewhere(&self.name_path.join("."))
4909 || contains_disable_through_calls(stmt.as_sem_node())) =>
4910 {
4911 self.entering_block = false;
4912 let exit = self.alloc_block();
4913 self.target_depth.insert(exit, self.owned_scopes.len());
4914 // LRM 9.6.2: another process's `disable` of this block resumes
4915 // the process at `exit`, wherever in the block it is suspended.
4916 let scoped = is_disabled_somewhere(&name);
4917 if scoped {
4918 self.scope_chain
4919 .push((name.clone(), exit, self.frame_stack.len()));
4920 }
4921 // `disable p.b.x;` of another process names the block by its
4922 // path from the process's outermost block: a second scope
4923 // under that dotted label tells same-named blocks apart.
4924 let dotted = self.name_path.join(".");
4925 let path_scoped = dotted.contains('.') && is_disabled_somewhere(&dotted);
4926 if path_scoped {
4927 self.scope_chain
4928 .push((dotted.clone(), exit, self.frame_stack.len()));
4929 }
4930 // `disable fl.b1.blk;` goes through the fork's own label `fl`
4931 // (9.6.2): the branch's path under that label tells the
4932 // same-named blocks of sibling branches apart.
4933 let fork_key = self
4934 .fork_name
4935 .as_ref()
4936 .map(|f| format!("{f}.{dotted}"))
4937 .filter(|k| is_disabled_somewhere(k));
4938 let fork_scoped = fork_key.is_some();
4939 if let Some(key) = fork_key {
4940 self.scope_chain.push((key, exit, self.frame_stack.len()));
4941 }
4942 self.disable_targets
4943 .push((name, exit, self.frame_stack.len()));
4944 let end = self.walk_seq(live_statements(&stmt), cur);
4945 self.disable_targets.pop();
4946 if fork_scoped {
4947 self.scope_chain.pop();
4948 }
4949 if path_scoped {
4950 self.scope_chain.pop();
4951 }
4952 if scoped {
4953 self.scope_chain.pop();
4954 }
4955 self.close(end?, Terminator::Fallthrough(exit));
4956 Ok(exit)
4957 }
4958 Some(StatementBlockKind::Sequential) => {
4959 self.entering_block = false;
4960 self.walk_seq(live_statements(&stmt), cur)
4961 }
4962 Some(
4963 kind @ (StatementBlockKind::JoinAll
4964 | StatementBlockKind::JoinAny
4965 | StatementBlockKind::JoinNone),
4966 ) => {
4967 // Task 1 (PR-069/`docs/design/process.md` §7): construction
4968 // only, never scheduled. Each branch is walked as its own,
4969 // fully independent `ProcessCfg` (`OneShot` -- a fork
4970 // branch runs to completion or suspends, but never
4971 // re-triggers the way an `always` process does).
4972 //
4973 // Verified live: `>1` un-`begin`/`end`-wrapped branches
4974 // (`fork a; b; join`) are NOT `stmt.statements()`'s direct
4975 // elements -- slang groups them into one `List` node first
4976 // (the same generic multi-statement wrapper `walk_stmt`'s
4977 // own `StatementKind::List` arm already flattens elsewhere),
4978 // so `statements()` here returns exactly one `List` element,
4979 // not one element per branch. A single, un-wrapped branch
4980 // (`fork a; join`) has no such wrapper. Flatten exactly one
4981 // `List` level when present, never more (a `List`
4982 // containing a nested `List` is not a shape any fixture
4983 // this task constructs, so silently over-flattening would
4984 // risk merging two real branches into one).
4985 let branch_stmts = live_statements(&stmt);
4986 let branch_stmts = match branch_stmts.as_slice() {
4987 [only] if only.kind() == StatementKind::List => live_statements(only),
4988 _ => branch_stmts,
4989 };
4990 self.entering_block = false;
4991 let fork_name = Some(block_name(&stmt)).filter(|n| !n.is_empty());
4992 // LRM 9.6.2: a `disable` of one branch's own named outermost
4993 // block, reachable from the parent (after this fork
4994 // statement), a sibling branch, or deeper still (threaded
4995 // into each branch's own `Inherited::known_fork_labels`
4996 // below) -- see `CfgBuilder::fork_branch_names`'s own doc
4997 // comment. Computed from a reference: `branch_stmts` is
4998 // still needed, un-consumed, by the `.into_iter()` build
4999 // below.
5000 // The fork block's own name (`fork : NAME`) tags every branch
5001 // process too (`ProcessCfg::disable_names`), so a later
5002 // `disable NAME;` of the parent, after the join, ends the
5003 // branches still running (LRM 9.6.2: all activities enabled
5004 // within the named block end).
5005 let new_branch_names: Vec<String> = branch_stmts
5006 .iter()
5007 .filter_map(|s| peer_block(s).map(|p| p.name))
5008 .collect();
5009 let known_fork_labels: Vec<String> = self
5010 .fork_branch_names
5011 .iter()
5012 .filter(|n| !self.fork_block_names.contains(n))
5013 .cloned()
5014 .chain(new_branch_names.iter().cloned())
5015 .collect();
5016 let ancestor_blocks: Vec<String> = self
5017 .ancestor_blocks
5018 .iter()
5019 .cloned()
5020 .chain(self.disable_targets.iter().map(|(n, _, _)| n.clone()))
5021 .collect();
5022 // Equal inherited context and statement give an equal CFG
5023 // (a fresh builder is seeded from `Inherited` alone), so a
5024 // task inlined at many call sites builds each of its forks'
5025 // branches once and the lowering compiles each once
5026 // ([`ProcessCfg::share_id`]). Not inside a recursive
5027 // activation (its capture temporaries are per site) or
5028 // under a clock inference.
5029 let memoizable = self.rec.is_none()
5030 && self.rec_up.is_empty()
5031 && self.global_clock.is_none()
5032 && self.checker_clock.is_none();
5033 let mut branches: Vec<ProcessCfg<'d>> = branch_stmts
5034 .into_iter()
5035 .map(|branch_stmt| {
5036 let key = memoizable.then(|| {
5037 MemoKey::Fork(ForkBranchKey {
5038 stmt: (stmt_addr(&branch_stmt), branch_stmt.kind() as u16),
5039 fork_name: fork_name.clone(),
5040 default_clocking: self.default_clocking.map(|c| c.uid()),
5041 this_var: self.this_stack.last().map(|t| t.uid()),
5042 ref_aliases: self
5043 .ref_stack
5044 .iter()
5045 .map(|(a, b)| (a.uid(), b.uid()))
5046 .collect(),
5047 rec_outer: self.rec_outer.clone(),
5048 labels: known_fork_labels.clone(),
5049 ancestors: ancestor_blocks.clone(),
5050 })
5051 });
5052 if let Some(k) = &key
5053 && let Some(hit) = self.fork_memo.borrow().get(k)
5054 {
5055 return Ok(hit.clone());
5056 }
5057 let mut cfg = build_cfg_in(
5058 &branch_stmt,
5059 ProcessLoopKind::OneShot,
5060 fork_name.clone(),
5061 self.default_clocking,
5062 None,
5063 self.checker_clock,
5064 Inherited {
5065 this_var: self.this_stack.last().copied(),
5066 ref_aliases: self.ref_stack.clone(),
5067 global_clock: self.global_clock,
5068 detached_trigger: false,
5069 forced_extern: None,
5070 rec_outer: self.rec_outer.clone(),
5071 fork_memo: Some(self.fork_memo.clone()),
5072 rec: self.rec.clone(),
5073 rec_up: self.rec_up.clone(),
5074 known_fork_labels: known_fork_labels.clone(),
5075 ancestor_blocks: ancestor_blocks.clone(),
5076 },
5077 self.virtual_tasks.clone(),
5078 )?;
5079 if let Some(k) = key {
5080 cfg.share_id = Some(
5081 NEXT_SHARE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
5082 );
5083 self.fork_memo.borrow_mut().insert(k, cfg.clone());
5084 }
5085 Ok(cfg)
5086 })
5087 .collect::<Result<_, LoweringError>>()?;
5088 for b in &mut branches {
5089 b.captured = captured_vars(b);
5090 // An aliased `ref` formal is the branch's storage too.
5091 b.captured
5092 .retain(|c| !self.ref_stack.iter().any(|(f, _)| f.uid() == c.uid()));
5093 // A branch inside an inlined class-method task shares the
5094 // forking activation's `this`.
5095 if let Some(t) = self.this_stack.last()
5096 && !b.captured.iter().any(|c| c.uid() == t.uid())
5097 {
5098 b.captured.push(*t);
5099 }
5100 }
5101 // The parent keeps these labels resolvable for the rest of
5102 // its own walk (not popped like `disable_targets`): LRM
5103 // 9.6.2 lets a branch be disabled for the rest of the
5104 // process's life, not only while this `fork` statement is
5105 // still being walked.
5106 self.fork_branch_names.extend(new_branch_names);
5107 if let Some(f) = &fork_name {
5108 self.fork_branch_names.push(f.clone());
5109 self.fork_block_names.push(f.clone());
5110 }
5111 let join_blk = self.alloc_block();
5112 self.close(
5113 cur,
5114 Terminator::ForkJoin {
5115 kind,
5116 branches,
5117 join_blk,
5118 },
5119 );
5120 Ok(join_blk)
5121 }
5122 None => Err(LoweringError::malformed(
5123 "Block statement with no block_kind (unexpected AST shape)",
5124 )),
5125 }
5126 }
5127
5128 fn walk_seq(
5129 &mut self,
5130 stmts: Vec<Statement<'d>>,
5131 mut cur: BlockId,
5132 ) -> Result<BlockId, LoweringError> {
5133 // LRM 14.11: a `##0` whose clocking event already occurred in this
5134 // time step "shall have no effect". Statically known exactly when the
5135 // process was just released by that event (`##N;` or
5136 // `@(cb);` on the default clocking, a `##0` included once it has waited) and only untimed, call-free
5137 // statements ran since; any other `##0` waits for the event (which is
5138 // right whenever the event has not yet fired this step).
5139 let mut released = false;
5140 for s in stmts {
5141 let s = unwrap_invalid(s);
5142 match self.default_clocking_wait(&s) {
5143 Some(0) if released => {
5144 if let Some(body) = s.body()
5145 && body.kind() != StatementKind::Empty
5146 {
5147 cur = self.walk_stmt(body, cur)?;
5148 }
5149 continue;
5150 }
5151 Some(_) => released = s.body().is_none_or(|b| b.kind() == StatementKind::Empty),
5152 None => {
5153 released = released && Self::untimed_simple(&s);
5154 }
5155 }
5156 cur = self.walk_stmt(s, cur)?;
5157 }
5158 Ok(cur)
5159 }
5160
5161 /// The cycle count of `##N` / `@(cb)` (count 1) on the default clocking
5162 /// when `stmt` is such a timed statement (see [`Self::walk_seq`]).
5163 fn default_clocking_wait(&self, stmt: &Statement<'d>) -> Option<u32> {
5164 let cb = self.default_clocking?;
5165 if stmt.kind() != StatementKind::Timed {
5166 return None;
5167 }
5168 let tc = stmt.timing_control()?;
5169 // A predicate, not a lowering: any other timing control is simply not
5170 // a wait on the default clocking (the next `##0` then waits).
5171 if tc.kind() == TimingControlKind::CycleDelay {
5172 first_expression_child(&tc)?
5173 .constant_value()?
5174 .as_i64()
5175 .and_then(|v| u32::try_from(v).ok())
5176 } else if tc.kind() == TimingControlKind::SignalEvent && tc.edge() == EdgeKind::None {
5177 let sym = first_expression_child(&tc)?.referenced_symbol()?;
5178 (sym.kind() == SymbolKind::ClockingBlock && sym.uid() == cb.uid()).then_some(1)
5179 } else {
5180 None
5181 }
5182 }
5183
5184 /// A statement that takes no time and calls no user subroutine: a system
5185 /// call or an untimed assignment.
5186 fn untimed_simple(stmt: &Statement<'d>) -> bool {
5187 stmt.kind() == StatementKind::ExpressionStatement
5188 && stmt.expr().is_some_and(|e| {
5189 (e.kind() == ExpressionKind::Call && e.is_system_call() == Some(true))
5190 || (e.kind() == ExpressionKind::Assignment && e.assignment_timing().is_none())
5191 })
5192 }
5193
5194 /// PR-044: `if`/`else` always expands to `Terminator::Branch`, regardless
5195 /// of whether either branch contains a suspend -- this is orthogonal to
5196 /// (never triggers) suspend splitting, which is exactly what
5197 /// `if_else_does_not_split_the_cfg` (`tests/process_cfg.rs`) proves: the
5198 /// number of *additional* blocks a wrapping suspend contributes is the
5199 /// same with or without the branch present.
5200 fn walk_conditional(
5201 &mut self,
5202 stmt: Statement<'d>,
5203 cur: BlockId,
5204 ) -> Result<BlockId, LoweringError> {
5205 if violation_check(&stmt).is_some() || is_clause_if(&stmt) {
5206 return self.walk_checked_if(stmt, cur);
5207 }
5208 let cond = stmt.expressions().into_iter().next().ok_or_else(|| {
5209 LoweringError::malformed("Conditional statement with no condition expression")
5210 })?;
5211 let then_stmt = stmt
5212 .then_branch()
5213 .ok_or_else(|| LoweringError::malformed("Conditional statement with no then-branch"))?;
5214 let else_stmt = stmt.else_branch();
5215
5216 // A constant predicate picks its arm at elaboration; only the live
5217 // arm becomes blocks (see `constant_if_arm`).
5218 if let Some(taken) = constant_if_arm(&stmt) {
5219 return match if taken { Some(then_stmt) } else { else_stmt } {
5220 Some(live) => self.walk_stmt(live, cur),
5221 None => Ok(cur),
5222 };
5223 }
5224
5225 // A call in the condition to a function that is inlined (a `fork` in
5226 // its body, an event output formal) runs ahead of the test; the
5227 // condition reads its result variable.
5228 let cur = self
5229 .hoist_suspending_calls_in(cond.as_sem_node(), cur)?
5230 .unwrap_or(cur);
5231 let then_blk = self.alloc_block();
5232 let else_blk = self.alloc_block();
5233 self.close(
5234 cur,
5235 Terminator::Branch {
5236 cond,
5237 then_blk,
5238 else_blk,
5239 },
5240 );
5241
5242 let then_exit = self.walk_stmt(then_stmt, then_blk)?;
5243 let else_exit = match else_stmt {
5244 Some(e) => self.walk_stmt(e, else_blk)?,
5245 None => else_blk,
5246 };
5247
5248 let join = self.alloc_block();
5249 self.close(then_exit, Terminator::Fallthrough(join));
5250 self.close(else_exit, Terminator::Fallthrough(join));
5251 Ok(join)
5252 }
5253
5254 /// A `unique`/`unique0`/`priority` `if` series (§12.4.2): one
5255 /// [`Terminator::Case`] over the whole series, `arms[i]` the then-statement
5256 /// of its i-th condition, `default_blk` the final `else` (empty without
5257 /// one), so the dispatch can report a violation (§12.4.2.1) where the
5258 /// series executes.
5259 fn walk_checked_if(
5260 &mut self,
5261 stmt: Statement<'d>,
5262 cur: BlockId,
5263 ) -> Result<BlockId, LoweringError> {
5264 let chain = if_series(stmt).map_err(|why| {
5265 LoweringError::unsupported(format!("{why} in an if series (malformed statement)"))
5266 })?;
5267 let arms: Vec<BlockId> = chain.thens.iter().map(|_| self.alloc_block()).collect();
5268 let default_blk = self.alloc_block();
5269 let join = self.alloc_block();
5270 self.close(
5271 cur,
5272 Terminator::Case {
5273 stmt,
5274 arms: arms.clone(),
5275 default_blk,
5276 },
5277 );
5278 for (then_stmt, &arm) in chain.thens.iter().zip(&arms) {
5279 let exit = self.walk_stmt(*then_stmt, arm)?;
5280 self.close(exit, Terminator::Fallthrough(join));
5281 }
5282 let default_exit = match chain.otherwise {
5283 Some(e) => self.walk_stmt(e, default_blk)?,
5284 None => default_blk,
5285 };
5286 self.close(default_exit, Terminator::Fallthrough(join));
5287 Ok(join)
5288 }
5289
5290 /// PR-006/PR-007: the one place a `BasicBlock` closes with
5291 /// `Terminator::Suspend` from a `Timed` statement. `Delay`/`SignalEvent`
5292 /// (posedge/negedge only) are the slice-scope half; Task 1 adds
5293 /// `EventList` (as `SuspendPoint::EdgeList`) and `CycleDelay`. **Task 6
5294 /// (CG-081/CG-171)** adds real handling for `ImplicitEvent` (as
5295 /// `SuspendPoint::ImplicitComb`) and `OneStepDelay` (as `SuspendPoint::
5296 /// OneStep`), and revises `CycleDelay` to carry its inferred clock
5297 /// signal/edge as real payload instead of discarding it. `Delay3` (not
5298 /// applicable to a procedural suspend point at all), `BlockEventList` (a
5299 /// verified, hard upstream `sv-lang`=0.1.9 API gap) and `RepeatedEvent`
5300 /// (verified structurally unreachable at this dispatch point -- see that
5301 /// arm below) remain named [`LoweringError`]s.
5302 /// See [`Self::walk_stmt`]'s doc comment: `TimingControlKind` is also
5303 /// `#[non_exhaustive]` upstream, so this dispatch is likewise an
5304 /// `if`/`else-if` chain (pinned by
5305 /// `determinism::kind_guard::timing_control_kind_count_matches_pinned_
5306 /// sv_lang_version`, `TimingControlKind::COUNT == 10`), not a literal
5307 /// `match`.
5308 fn walk_timed(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
5309 let tc = stmt
5310 .timing_control()
5311 .ok_or_else(|| LoweringError::malformed("Timed statement with no timing control"))?;
5312 if !class_event_wait_terms(&tc)?.is_empty() && class_event_wait_is_mixed(&tc) {
5313 // `@(ce or sig ...)` (LRM 1800-2023 9.4.2.1, 15.5): the process
5314 // waits on a class event (a runtime queue) and on signals (the
5315 // sensitivity table) at once. The leaf statement queues it on the
5316 // class events without blocking, the suspend is the signal
5317 // wait, and whichever wakes it first wins: the resume block leaves
5318 // the class queues (`ClassEventLeave`), and a class trigger
5319 // desensitises the signal side (`Engine::apply_sync_wake`).
5320 let guarded = stmt
5321 .body()
5322 .ok_or_else(|| LoweringError::malformed("Timed statement with no guarded body"))?;
5323 self.skip_class_event_terms = true;
5324 let point = self.suspend_point(tc, None);
5325 self.skip_class_event_terms = false;
5326 let point = point?;
5327 let term_iffs = class_event_term_iffs(&tc);
5328 if term_iffs.iter().any(Option::is_some) {
5329 // A class term with an `iff`: when a class term woke the
5330 // process (a signal term's own `iff` was judged by the
5331 // sensitivity table), the first fired class term whose `iff`
5332 // holds continues it, and none waits on the whole list again
5333 // (LRM 9.4.2.3, each term judged on its own).
5334 if term_iffs.len() > 64 {
5335 return Err(LoweringError::unsupported(
5336 "an `@(a or b ...)` list of more than 64 class `event` terms with an `iff`",
5337 ));
5338 }
5339 let header = self.alloc_block();
5340 let body = self.alloc_block();
5341 let exit = self.alloc_block();
5342 let resume = self.alloc_block();
5343 self.blocks[resume.0 as usize]
5344 .bindings
5345 .push(ArgBinding::ClassEventLeave);
5346 self.close(
5347 cur,
5348 Terminator::Loop {
5349 header,
5350 body,
5351 exit,
5352 entry: header,
5353 },
5354 );
5355 self.close(header, Terminator::Fallthrough(body));
5356 self.push(body, stmt);
5357 self.close(
5358 body,
5359 Terminator::Suspend {
5360 point,
5361 resume_blk: resume,
5362 },
5363 );
5364 let first = self.alloc_block();
5365 self.close(
5366 resume,
5367 Terminator::Switch {
5368 on: SwitchOn::ClassFired(None),
5369 arms: vec![exit, first],
5370 default_blk: exit,
5371 report: None,
5372 },
5373 );
5374 let mut at = first;
5375 for (k, iff) in term_iffs.iter().enumerate() {
5376 let chk = self.alloc_block();
5377 let next = if k + 1 == term_iffs.len() {
5378 header
5379 } else {
5380 self.alloc_block()
5381 };
5382 self.close(
5383 at,
5384 Terminator::Switch {
5385 on: SwitchOn::ClassFired(Some(k as u32)),
5386 arms: vec![next, chk],
5387 default_blk: next,
5388 report: None,
5389 },
5390 );
5391 match iff {
5392 Some(cond) => self.close(
5393 chk,
5394 Terminator::Branch {
5395 cond: *cond,
5396 then_blk: exit,
5397 else_blk: next,
5398 },
5399 ),
5400 None => self.close(chk, Terminator::Fallthrough(exit)),
5401 }
5402 at = next;
5403 }
5404 return self.walk_stmt(guarded, exit);
5405 }
5406 self.push(cur, stmt);
5407 let resume = self.alloc_block();
5408 self.blocks[resume.0 as usize]
5409 .bindings
5410 .push(ArgBinding::ClassEventLeave);
5411 self.close(
5412 cur,
5413 Terminator::Suspend {
5414 point,
5415 resume_blk: resume,
5416 },
5417 );
5418 return self.walk_stmt(guarded, resume);
5419 }
5420 if !class_event_wait_terms(&tc)?.is_empty() {
5421 // `@e` on a class event (LRM 1800-2023 15.5): the statement is the
5422 // leaf that queues the process on the event's runtime object; the
5423 // suspend after it lets the scheduler apply the block request,
5424 // like a blocking `semaphore.get`. Codegen lowers only the wait of
5425 // this leaf (the guarded body follows in the resumed block).
5426 let guarded = stmt
5427 .body()
5428 .ok_or_else(|| LoweringError::malformed("Timed statement with no guarded body"))?;
5429 let term_iffs = class_event_term_iffs(&tc);
5430 if term_iffs.iter().any(Option::is_some) {
5431 // `@(a iff x or b iff y ...)` of class events: the runtime
5432 // records which terms fired (`sv_rt_fired_mask`), the resume
5433 // takes the first fired term whose `iff` holds, and waits
5434 // again when none does (each term judged on its own, 9.4.2.3).
5435 if term_iffs.len() > 64 {
5436 return Err(LoweringError::unsupported(
5437 "an `@(a or b ...)` list of more than 64 class `event` terms with an `iff`",
5438 ));
5439 }
5440 let header = self.alloc_block();
5441 let body = self.alloc_block();
5442 let exit = self.alloc_block();
5443 let resume = self.alloc_block();
5444 self.close(
5445 cur,
5446 Terminator::Loop {
5447 header,
5448 body,
5449 exit,
5450 entry: header,
5451 },
5452 );
5453 self.close(header, Terminator::Fallthrough(body));
5454 self.push(body, stmt);
5455 self.close(
5456 body,
5457 Terminator::Suspend {
5458 point: SuspendPoint::ByRequest,
5459 resume_blk: resume,
5460 },
5461 );
5462 let mut at = resume;
5463 for (k, iff) in term_iffs.iter().enumerate() {
5464 let chk = self.alloc_block();
5465 let next = if k + 1 == term_iffs.len() {
5466 header
5467 } else {
5468 self.alloc_block()
5469 };
5470 self.close(
5471 at,
5472 Terminator::Switch {
5473 on: SwitchOn::FiredBit(k as u32),
5474 arms: vec![next, chk],
5475 default_blk: next,
5476 report: None,
5477 },
5478 );
5479 match iff {
5480 Some(cond) => self.close(
5481 chk,
5482 Terminator::Branch {
5483 cond: *cond,
5484 then_blk: exit,
5485 else_blk: next,
5486 },
5487 ),
5488 None => self.close(chk, Terminator::Fallthrough(exit)),
5489 }
5490 at = next;
5491 }
5492 return self.walk_stmt(guarded, exit);
5493 }
5494 if let Some(iff) = extract_iff(&tc) {
5495 // `@(e iff c)` (LRM 1800-2023 9.4.2.3): the event wakes the
5496 // process, which continues only if `c` holds then; otherwise
5497 // it waits for the event again. A loop around the plain wait.
5498 let header = self.alloc_block();
5499 let body = self.alloc_block();
5500 let exit = self.alloc_block();
5501 let resume = self.alloc_block();
5502 self.close(
5503 cur,
5504 Terminator::Loop {
5505 header,
5506 body,
5507 exit,
5508 entry: header,
5509 },
5510 );
5511 self.close(header, Terminator::Fallthrough(body));
5512 self.push(body, stmt);
5513 self.close(
5514 body,
5515 Terminator::Suspend {
5516 point: SuspendPoint::ByRequest,
5517 resume_blk: resume,
5518 },
5519 );
5520 self.close(
5521 resume,
5522 Terminator::Branch {
5523 cond: iff,
5524 then_blk: exit,
5525 else_blk: header,
5526 },
5527 );
5528 return self.walk_stmt(guarded, exit);
5529 }
5530 self.push(cur, stmt);
5531 let resume = self.alloc_block();
5532 self.close(
5533 cur,
5534 Terminator::Suspend {
5535 point: SuspendPoint::ByRequest,
5536 resume_blk: resume,
5537 },
5538 );
5539 return self.walk_stmt(guarded, resume);
5540 }
5541 self.allow_dynamic_cycle = true;
5542 let point = self.suspend_point(tc, stmt.body());
5543 self.allow_dynamic_cycle = false;
5544 let point = point?;
5545 let guarded = stmt
5546 .body()
5547 .ok_or_else(|| LoweringError::malformed("Timed statement with no guarded body"))?;
5548 if let Some(count) = self.dynamic_cycle_count.take() {
5549 // `##(expr)` with a run-time count (LRM 14.11): `repeat (expr)`
5550 // around one `##1` wait, the count evaluated once on entry. A
5551 // count <= 0 waits for no clock event.
5552 let entry = self.alloc_block();
5553 self.close(cur, Terminator::Fallthrough(entry));
5554 let counter = self.alloc_repeat_counter();
5555 self.set_repeat_init(entry, counter, RepeatCount::Expr(count));
5556 let header = self.alloc_block();
5557 let body = self.alloc_block();
5558 let exit = self.alloc_block();
5559 let resume = self.alloc_block();
5560 self.close(
5561 entry,
5562 Terminator::Loop {
5563 header,
5564 body,
5565 exit,
5566 entry: header,
5567 },
5568 );
5569 self.close(
5570 header,
5571 Terminator::RepeatCheck {
5572 counter,
5573 then_blk: body,
5574 else_blk: exit,
5575 },
5576 );
5577 self.close(
5578 body,
5579 Terminator::Suspend {
5580 point,
5581 resume_blk: resume,
5582 },
5583 );
5584 self.set_repeat_decrement(resume, counter);
5585 self.close(resume, Terminator::Fallthrough(header));
5586 return self.walk_stmt(guarded, exit);
5587 }
5588 let resume = self.alloc_block();
5589 self.close(
5590 cur,
5591 Terminator::Suspend {
5592 point,
5593 resume_blk: resume,
5594 },
5595 );
5596 self.walk_stmt(guarded, resume)
5597 }
5598
5599 /// `lhs = #d rhs;`, `lhs = @(e) rhs;`, `lhs = repeat (n) @(e) rhs;` (LRM
5600 /// 1800-2023 §10.4.1): the right-hand side is evaluated now into a
5601 /// compiler temporary ([`ArgBinding::Capture`]), the process waits, then
5602 /// the temporary is assigned ([`ArgBinding::AssignCaptured`]).
5603 fn walk_intra_assignment_timing(
5604 &mut self,
5605 assign: Expression<'d>,
5606 cur: BlockId,
5607 ) -> Result<BlockId, LoweringError> {
5608 let tc = assign
5609 .assignment_timing()
5610 .and_then(|n| n.as_timing_control())
5611 .ok_or_else(|| LoweringError::malformed("assignment timing is not a timing control"))?;
5612 let target = assign
5613 .left()
5614 .ok_or_else(|| LoweringError::malformed("assignment with no target"))?;
5615 let value = assign
5616 .right()
5617 .ok_or_else(|| LoweringError::malformed("assignment with no value"))?;
5618 let temp = self.captures.len() as u32;
5619 self.captures.push(target);
5620 // A fresh block: its bindings run before anything else in it, and
5621 // the capture must follow `cur`'s own statements.
5622 let capture = self.alloc_block();
5623 self.close(cur, Terminator::Fallthrough(capture));
5624 self.blocks[capture.0 as usize]
5625 .bindings
5626 .push(ArgBinding::Capture { temp, value });
5627 self.drive_clocking = drive_clocking_of(target);
5628 self.drive_via = drive_via_of(target);
5629 let after = if tc.kind() == TimingControlKind::RepeatedEvent {
5630 // `repeat (n) @(e)`: wait for `n` occurrences of `e` -- a counted
5631 // loop around one suspend (a count <= 0 waits for none).
5632 let count = first_expression_child(&tc).ok_or_else(|| {
5633 LoweringError::malformed("repeated event control with no count expression")
5634 })?;
5635 let event = tc
5636 .children()
5637 .into_iter()
5638 .find_map(|n| n.as_timing_control())
5639 .ok_or_else(|| {
5640 LoweringError::malformed("repeated event control with no event control")
5641 })?;
5642 let point = self.suspend_point(event, None)?;
5643 let counter = self.alloc_repeat_counter();
5644 self.set_repeat_init(capture, counter, RepeatCount::Expr(count));
5645 let header = self.alloc_block();
5646 let body = self.alloc_block();
5647 let exit = self.alloc_block();
5648 let resume = self.alloc_block();
5649 self.close(
5650 capture,
5651 Terminator::Loop {
5652 header,
5653 body,
5654 exit,
5655 entry: header,
5656 },
5657 );
5658 self.close(
5659 header,
5660 Terminator::RepeatCheck {
5661 counter,
5662 then_blk: body,
5663 else_blk: exit,
5664 },
5665 );
5666 self.close(
5667 body,
5668 Terminator::Suspend {
5669 point,
5670 resume_blk: resume,
5671 },
5672 );
5673 self.set_repeat_decrement(resume, counter);
5674 self.close(resume, Terminator::Fallthrough(header));
5675 exit
5676 } else {
5677 let point = self.suspend_point(tc, None)?;
5678 let resume = self.alloc_block();
5679 self.close(
5680 capture,
5681 Terminator::Suspend {
5682 point,
5683 resume_blk: resume,
5684 },
5685 );
5686 resume
5687 };
5688 self.drive_clocking = None;
5689 self.drive_via = None;
5690 self.blocks[after.0 as usize]
5691 .bindings
5692 .push(ArgBinding::AssignCaptured {
5693 temp,
5694 target,
5695 nba: false,
5696 });
5697 Ok(after)
5698 }
5699
5700 /// `lhs <= @(e) rhs;`, `lhs <= repeat (n) @(e) rhs;` (LRM 1800-2023
5701 /// §10.4.2, §9.4.5): the right-hand side (and the repeat count) are
5702 /// evaluated now into the compiler temporaries of this process, which
5703 /// goes straight on; a synthetic `join_none` branch, spawned here with
5704 /// copies of those temporaries ([`ProcessCfg::inherited`]), waits for
5705 /// the event control and then stages the nonblocking update.
5706 ///
5707 /// Deviation: the branch is a child of this process like any forked
5708 /// one, so a later `wait fork;` waits for the pending update and
5709 /// `disable fork;` cancels it.
5710 fn walk_nba_event(
5711 &mut self,
5712 stmt: Statement<'d>,
5713 assign: Expression<'d>,
5714 cur: BlockId,
5715 ) -> Result<BlockId, LoweringError> {
5716 let tc = assign
5717 .assignment_timing()
5718 .and_then(|n| n.as_timing_control())
5719 .ok_or_else(|| LoweringError::malformed("assignment timing is not a timing control"))?;
5720 let target = assign
5721 .left()
5722 .ok_or_else(|| LoweringError::malformed("assignment with no target"))?;
5723 let value = assign
5724 .right()
5725 .ok_or_else(|| LoweringError::malformed("assignment with no value"))?;
5726 let temp = self.captures.len() as u32;
5727 self.captures.push(target);
5728 let capture = self.alloc_block();
5729 self.close(cur, Terminator::Fallthrough(capture));
5730 self.blocks[capture.0 as usize]
5731 .bindings
5732 .push(ArgBinding::Capture { temp, value });
5733
5734 // The branch: its own builder, with this walk's context.
5735 let mut b = CfgBuilder::new();
5736 b.this_stack.extend(self.this_stack.last().copied());
5737 b.ref_stack = self.ref_stack.clone();
5738 b.global_clock = self.global_clock;
5739 b.virtual_tasks = self.virtual_tasks.clone();
5740 b.fork_branch_names = self.fork_branch_names.clone();
5741 b.ancestor_blocks = self.ancestor_blocks.clone();
5742 b.default_clocking = self.default_clocking;
5743 b.drive_clocking = drive_clocking_of(target);
5744 b.drive_via = drive_via_of(target);
5745 b.checker_clock = self.checker_clock;
5746 b.captures.push(target);
5747 let mut inherited = vec![InheritedSlot::Temp {
5748 parent: temp,
5749 child: 0,
5750 }];
5751 let entry = b.alloc_block();
5752 let event_roots = vec![target.as_sem_node(), tc.as_sem_node()];
5753 // `lhs <= ##(n) v` with a run-time count (LRM 14.11, 14.16): the count
5754 // is evaluated here and the branch loops one `##1` wait that often,
5755 // like `repeat (n) @(clk)`.
5756 let mut dyn_cycle: Option<(Expression<'d>, SuspendPoint)> = None;
5757 let mut const_point: Option<SuspendPoint> = None;
5758 if tc.kind() == TimingControlKind::CycleDelay {
5759 b.allow_dynamic_cycle = true;
5760 let point = b.suspend_point(tc, None);
5761 b.allow_dynamic_cycle = false;
5762 let point = point?;
5763 match b.dynamic_cycle_count.take() {
5764 Some(count) => dyn_cycle = Some((count, point)),
5765 None => const_point = Some(point),
5766 }
5767 }
5768 let after = if dyn_cycle.is_some() || tc.kind() == TimingControlKind::RepeatedEvent {
5769 let (count, point) = if let Some(d) = dyn_cycle.take() {
5770 d
5771 } else {
5772 let count = first_expression_child(&tc).ok_or_else(|| {
5773 LoweringError::malformed("repeated event control with no count expression")
5774 })?;
5775 let event = tc
5776 .children()
5777 .into_iter()
5778 .find_map(|n| n.as_timing_control())
5779 .ok_or_else(|| {
5780 LoweringError::malformed("repeated event control with no event control")
5781 })?;
5782 (count, b.suspend_point(event, None)?)
5783 };
5784 // The count is evaluated here, in this process.
5785 let parent_counter = self.alloc_repeat_counter();
5786 self.set_repeat_init(capture, parent_counter, RepeatCount::Expr(count));
5787 let counter = b.alloc_repeat_counter();
5788 inherited.push(InheritedSlot::Counter {
5789 parent: parent_counter,
5790 child: counter,
5791 });
5792 b.set_repeat_init(entry, counter, RepeatCount::Inherited(count));
5793 let header = b.alloc_block();
5794 let body = b.alloc_block();
5795 let exit = b.alloc_block();
5796 let resume = b.alloc_block();
5797 b.close(
5798 entry,
5799 Terminator::Loop {
5800 header,
5801 body,
5802 exit,
5803 entry: header,
5804 },
5805 );
5806 b.close(
5807 header,
5808 Terminator::RepeatCheck {
5809 counter,
5810 then_blk: body,
5811 else_blk: exit,
5812 },
5813 );
5814 b.close(
5815 body,
5816 Terminator::Suspend {
5817 point,
5818 resume_blk: resume,
5819 },
5820 );
5821 b.set_repeat_decrement(resume, counter);
5822 b.close(resume, Terminator::Fallthrough(header));
5823 exit
5824 } else {
5825 let point = match const_point.take() {
5826 Some(p) => p,
5827 None => b.suspend_point(tc, None)?,
5828 };
5829 let resume = b.alloc_block();
5830 b.close(
5831 entry,
5832 Terminator::Suspend {
5833 point,
5834 resume_blk: resume,
5835 },
5836 );
5837 resume
5838 };
5839 b.blocks[after.0 as usize]
5840 .bindings
5841 .push(ArgBinding::AssignCaptured {
5842 temp: 0,
5843 target,
5844 nba: true,
5845 });
5846 b.close(after, Terminator::End { loop_back: None });
5847 let inlined_locals = std::mem::take(&mut b.inlined_locals);
5848 let captures = std::mem::take(&mut b.captures);
5849 let ref_bindings = b.take_ref_bindings();
5850 let disable_scopes = b.take_disable_scopes();
5851 let disable_broadcast = std::mem::take(&mut b.disable_broadcast);
5852 let fork_branch_names = std::mem::take(&mut b.fork_branch_names);
5853 let mut branch = ProcessCfg {
5854 blocks: b.finish(),
5855 entry,
5856 body: stmt,
5857 inlined_locals,
5858 captures,
5859 captured: Vec::new(),
5860 ref_bindings,
5861 disable_names: Vec::new(),
5862 fork_branch_names,
5863 disable_scopes,
5864 disable_broadcast,
5865 inherited,
5866 capture_types: Vec::new(),
5867 recursion: Recursion::None,
5868 share_id: None,
5869 };
5870 // It shares the variables its event control and target name.
5871 branch.captured = captured_in(event_roots);
5872 branch
5873 .captured
5874 .retain(|c| !self.ref_stack.iter().any(|(f, _)| f.uid() == c.uid()));
5875 if let Some(t) = self.this_stack.last()
5876 && !branch.captured.iter().any(|c| c.uid() == t.uid())
5877 {
5878 branch.captured.push(*t);
5879 }
5880 let join_blk = self.alloc_block();
5881 self.close(
5882 capture,
5883 Terminator::ForkJoin {
5884 kind: StatementBlockKind::JoinNone,
5885 branches: vec![branch],
5886 join_blk,
5887 },
5888 );
5889 Ok(join_blk)
5890 }
5891
5892 /// A signal event's `(signal, edge)` with a checker's `$inferred_clock` (17.4)
5893 /// replaced by the clocking event of the instantiation context. Any other
5894 /// event is returned as written. A checker instantiated where no clock is
5895 /// inferred is a loud error, never a wait on nothing.
5896 fn resolve_inferred_clock(
5897 &self,
5898 signal: Expression<'d>,
5899 edge: EdgeKind,
5900 ) -> Result<InferredClockEvent<'d>, LoweringError> {
5901 if is_global_clock_call(&signal) {
5902 // `@$global_clock` (14.14, 17.7's counter_model): the global
5903 // clocking's event.
5904 return self.global_clock.ok_or_else(|| {
5905 LoweringError::unsupported(
5906 "`@$global_clock` as a procedural event control needs a `global clocking` \
5907 declared in the process's instance or an enclosing one (LRM 14.14); none \
5908 is in scope",
5909 )
5910 });
5911 }
5912 if !is_inferred_clock_call(&signal) {
5913 return Ok((signal, edge, None));
5914 }
5915 self.checker_clock.ok_or_else(|| {
5916 LoweringError::unsupported(
5917 "`$inferred_clock` (LRM 17.4) as the event control of a checker procedure: the \
5918 checker is instantiated where no default clocking applies (14.12), so no \
5919 clock is inferred",
5920 )
5921 })
5922 }
5923
5924 /// The suspend point a timing control waits on; `guarded` is the
5925 /// statement an `@*` implicit event control guards (its reads are the
5926 /// sensitivity).
5927 fn suspend_point(
5928 &mut self,
5929 tc: TimingControl<'d>,
5930 guarded: Option<Statement<'d>>,
5931 ) -> Result<SuspendPoint<'d>, LoweringError> {
5932 let tc_kind = tc.kind();
5933
5934 let point = if tc_kind == TimingControlKind::Invalid {
5935 return Err(LoweringError::malformed("Invalid timing control kind"));
5936 } else if tc_kind == TimingControlKind::Delay {
5937 let expr = first_expression_child(&tc).ok_or_else(|| {
5938 LoweringError::malformed("Delay timing control with no delay expression")
5939 })?;
5940 SuspendPoint::Delay(expr, self.frame_stack.last().map(|f| f.0))
5941 } else if tc_kind == TimingControlKind::SignalEvent {
5942 let edge = tc.edge();
5943 let signal = first_expression_child(&tc).ok_or_else(|| {
5944 LoweringError::malformed("SignalEvent timing control with no signal expression")
5945 })?;
5946 reject_chandle_event(&signal)?;
5947 let iff = extract_iff(&tc);
5948 let (signal, edge, inferred_iff) = self.resolve_inferred_clock(signal, edge)?;
5949 // `@clock iff c` on an inferred clock that itself has an `iff`
5950 // needs both guards (a conjunction, LRM 17.4): a one-member
5951 // event list carries them; `Edge` has room for one only.
5952 if let (Some(a), Some(b)) = (iff, inferred_iff) {
5953 if edge == EdgeKind::PosEdge || edge == EdgeKind::NegEdge {
5954 self.note_edge_signal(signal, edge);
5955 }
5956 return Ok(SuspendPoint::EdgeList(
5957 vec![(signal, edge)],
5958 vec![vec![a, b]],
5959 ));
5960 }
5961 let iff = iff.or(inferred_iff);
5962 if edge != EdgeKind::PosEdge && edge != EdgeKind::NegEdge {
5963 // `@(a)` (any change) / `@(edge a)`: a one-entry event list,
5964 // resolved through the program's wait table like `@(a or b)`.
5965 // With `iff` (LRM 1800-2023 9.4.2.3: `@(e iff c)` waits for a
5966 // change of `e`, or for the event, and then for `c`) it is a
5967 // single `Edge` term carrying its guard.
5968 if iff.is_some() {
5969 SuspendPoint::Edge { signal, edge, iff }
5970 } else {
5971 SuspendPoint::EdgeList(vec![(signal, edge)], vec![Vec::new()])
5972 }
5973 } else {
5974 self.note_edge_signal(signal, edge);
5975 SuspendPoint::Edge { signal, edge, iff }
5976 }
5977 } else if tc_kind == TimingControlKind::EventList {
5978 // Task 1: `@(a or b or c)` -- wake on any member. Each child is
5979 // itself a nested `TimingControl` (in practice always
5980 // `SignalEvent`, per the grammar). A member's `iff` (LRM
5981 // 1800-2023 9.4.2.3) rides along per member in the point's
5982 // second field.
5983 let mut entries = Vec::new();
5984 let mut iffs = Vec::new();
5985 for child in tc.children() {
5986 let sub_tc = child.as_timing_control().ok_or_else(|| {
5987 LoweringError::malformed(
5988 "EventList timing control with a non-timing-control child",
5989 )
5990 })?;
5991 let edge = sub_tc.edge();
5992 let signal = first_expression_child(&sub_tc).ok_or_else(|| {
5993 LoweringError::malformed(
5994 "EventList member timing control with no signal expression",
5995 )
5996 })?;
5997 if self.skip_class_event_terms && is_class_event_expr(&signal) {
5998 continue;
5999 }
6000 reject_chandle_event(&signal)?;
6001 let iff = extract_iff(&sub_tc);
6002 let (signal, edge, inferred_iff) = self.resolve_inferred_clock(signal, edge)?;
6003 if edge == EdgeKind::PosEdge || edge == EdgeKind::NegEdge {
6004 self.note_edge_signal(signal, edge);
6005 }
6006 entries.push((signal, edge));
6007 iffs.push(iff.into_iter().chain(inferred_iff).collect());
6008 }
6009 if entries.is_empty() {
6010 return Err(LoweringError::malformed(
6011 "EventList timing control with no member event controls",
6012 ));
6013 }
6014 SuspendPoint::EdgeList(entries, iffs)
6015 } else if tc_kind == TimingControlKind::CycleDelay {
6016 // Task 1 shape, Task 6 (CG-171) revised semantics -- see
6017 // `SuspendPoint::CycleDelay`'s own doc comment for the full
6018 // reasoning. Still a named `LoweringError`, never a silent
6019 // "assume 1 clock unit", when there is no unambiguous single
6020 // clock in scope.
6021 let count_expr = first_expression_child(&tc).ok_or_else(|| {
6022 LoweringError::malformed("CycleDelay timing control with no count expression")
6023 })?;
6024 let constant = count_expr
6025 .constant_value()
6026 .and_then(|v| v.as_i64())
6027 .and_then(|v| u32::try_from(v).ok());
6028 let count = match constant {
6029 Some(c) => c,
6030 None if self.allow_dynamic_cycle => {
6031 self.dynamic_cycle_count = Some(count_expr);
6032 1
6033 }
6034 None => {
6035 return Err(LoweringError::unsupported(
6036 "a non-constant cycle delay `##(expr)` in an intra-assignment timing \
6037 control (LRM 14.11 allows a run-time count only as a statement \
6038 delay; this simulator needs a compile-time constant here)",
6039 ));
6040 }
6041 };
6042 // The default clocking block's event (LRM 1800-2023 §14.11),
6043 // else the process's own inferred clock.
6044 let (clock, edge) = match (
6045 self.drive_clocking.or(self.default_clocking),
6046 self.inferred_clock,
6047 ) {
6048 (Some(cb), _) => (cb, EdgeKind::BothEdges),
6049 (None, Some(Some((signal, edge)))) => (
6050 signal.referenced_symbol().ok_or_else(|| {
6051 LoweringError::unsupported("CycleDelay (`##N`) clock with no symbol")
6052 })?,
6053 edge,
6054 ),
6055 _ => {
6056 return Err(LoweringError::unsupported(
6057 "CycleDelay (`##N`) with no unambiguous single inferred clock in scope \
6058 (no enclosing posedge/negedge edge, or more than one distinct clock \
6059 signal/edge seen) -- ARCHITECTURE §6.5's own \"ClockDomain the scheduler \
6060 needs\" requirement",
6061 ));
6062 }
6063 };
6064 let via = if self.drive_clocking.is_some() {
6065 self.drive_via
6066 } else {
6067 None
6068 };
6069 SuspendPoint::CycleDelay {
6070 count,
6071 clock,
6072 edge,
6073 via,
6074 }
6075 } else if tc_kind == TimingControlKind::ImplicitEvent {
6076 // Task 6 (CG-081): `@*`/`@(*)`. See `SuspendPoint::ImplicitComb`'s
6077 // own doc comment for why the read-set is captured here (AST-only,
6078 // no live Analysis handle reaches this crate).
6079 let body_stmt = guarded.ok_or_else(|| {
6080 LoweringError::unsupported(
6081 "an `@*` event control with no guarded statement (an intra-assignment `@*`)",
6082 )
6083 })?;
6084 SuspendPoint::ImplicitComb(implicit_comb_read_set(&body_stmt))
6085 } else if tc_kind == TimingControlKind::RepeatedEvent {
6086 // Task 6 (CG-081): `repeat(count) @(...)`. **Verified,
6087 // structurally unreachable at this dispatch point, never a scope
6088 // choice** -- empirically probed against the pinned
6089 // `sv-lang`=0.1.9 parser (a throwaway test compiling `initial q
6090 // <= repeat(3) @(posedge clk) 1;` and dumping the AST): a
6091 // `RepeatedEventControl` node is NEVER the outer `TimingControl`
6092 // of a `StatementKind::Timed` node at all. Two independent
6093 // parser facts combine to make this so: (1) `Parser::
6094 // parseStatement`'s own dispatch switch (`Parser_statements.cpp`)
6095 // sends a leading `repeat` keyword straight to
6096 // `parseLoopStatement` (producing `StatementKind::RepeatLoop`,
6097 // already handled by `walk_stmt`/`is_ordinary_statement_kind`) --
6098 // it never reaches `parseTimingControl()` at all, so
6099 // `repeat(3) @(posedge clk) q = 1;` used directly as a statement
6100 // parses as "run `@(posedge clk) q = 1;` 3 times", not "wait for
6101 // clk's 3rd posedge, then run `q = 1;` once". (2) The ONLY
6102 // grammar position `parseTimingControl()`'s own `RepeatKeyword`
6103 // case (`Parser_expressions.cpp`) is ever reached from is
6104 // `parseSubExpression`'s `isPossibleDelayOrEventControl` check --
6105 // i.e. `RepeatedEventControl` is only ever constructible as an
6106 // INTRA-ASSIGNMENT timing control embedded in an `Assignment`
6107 // expression's own `.children()` (confirmed empirically: `q <=
6108 // repeat(3) @(posedge clk) 1;` parses as one
6109 // `StatementKind::ExpressionStatement` whose `Assignment`
6110 // expression's `children()` is `[TimingControl(RepeatedEvent),
6111 // NamedValue, Conversion]` -- verified against
6112 // `AssignmentExpression::visitExprs` in the vendored slang C++,
6113 // `if (timingControl) timingControl->visit(visitor); left()...;
6114 // right()...;`), never a `Statement::timing_control()` at all.
6115 // The intra-assignment form is lowered by
6116 // `walk_intra_assignment_timing` (the right-hand side captured in
6117 // a `Temporary` frame slot before the counted wait), which asks
6118 // this function only for the inner event control's suspend
6119 // point -- so reaching this arm means a malformed AST.
6120 return Err(LoweringError::unsupported(
6121 "RepeatedEvent timing control (`repeat(n) @(...)`) can never be the outer \
6122 TimingControlKind of a Timed statement -- `repeat` at statement-position \
6123 always parses as StatementKind::RepeatLoop instead, and RepeatedEventControl \
6124 is only ever producible as an intra-assignment timing control, which \
6125 walk_intra_assignment_timing lowers",
6126 ));
6127 } else if tc_kind == TimingControlKind::OneStepDelay {
6128 // Task 6 (CG-081): `#0` / the built-in `1step` delay -- see
6129 // `SuspendPoint::OneStep`'s own doc comment.
6130 SuspendPoint::OneStep
6131 } else if tc_kind == TimingControlKind::BlockEventList {
6132 // LRM 19.3: a block event list `@@(begin f)` is only ever a
6133 // covergroup's sampling event (lowered by `covergroup` block-event
6134 // registration); slang rejects it as a procedural timing control
6135 // at parse ("expected statement"), so this arm is unreachable.
6136 return Err(LoweringError::unsupported(
6137 "a block event list `@@(...)` as a procedural timing control (LRM 19.3: \
6138 only a covergroup sampling event)",
6139 ));
6140 } else if tc_kind == TimingControlKind::Delay3 {
6141 // Delay3Control (`#(d1,d2,d3)`) is gate-primitive rise/fall/
6142 // turn-off delay syntax (LRM 28.14) -- not applicable to a
6143 // procedural `Timed` statement's suspend point at all (CG-081's
6144 // own 8-kind enumeration excludes it), so this stays a named
6145 // `LoweringError` unconditionally, not a gap this task closes.
6146 return Err(LoweringError::unsupported(
6147 "Delay3 timing control (gate-primitive rise/fall/turn-off delay syntax -- not \
6148 applicable to a procedural statement's suspend point)",
6149 ));
6150 } else {
6151 // Unreachable as long as `determinism::kind_guard`'s pinned
6152 // `TimingControlKind::COUNT == 10` still holds -- see
6153 // `walk_stmt`'s identical reasoning for `StatementKind`.
6154 return Err(LoweringError::malformed(format!(
6155 "unrecognized TimingControlKind {tc_kind:?} (sv-lang-kinds version drift -- \
6156 see determinism::kind_guard's TimingControlKind pin)"
6157 )));
6158 };
6159 Ok(point)
6160 }
6161
6162 /// Task 3 (PR-006's shape from Task 1, real semantics from CG-169/
6163 /// CG-115 here): `wait(expr) stmt;` (LRM 9.4.3). Unlike every other
6164 /// `Timed`/`Wait`-family suspend point, this one does NOT
6165 /// unconditionally close `cur` with `Terminator::Suspend` -- LRM 9.4.3 is
6166 /// an *immediate check with a conditional suspend*: if `expr` is already
6167 /// true the moment execution reaches it, there is NO suspend/resume
6168 /// round-trip at all (straight fallthrough into the guarded body, same
6169 /// call); only a false evaluation actually suspends. This is
6170 /// structurally the same "sometimes there's no suspend point at all"
6171 /// shape `docs/design/process.md` §2 calls out by name, and it is
6172 /// represented here exactly like any other runtime-decided branch: a
6173 /// `Terminator::Branch` whose `then_blk` is the guarded body's own entry
6174 /// block (`resume`) and whose `else_blk` is a fresh block that alone
6175 /// carries the real `Terminator::Suspend{point: SuspendPoint::Level(cond),
6176 /// resume_blk: resume}` -- `resume` is deliberately the SAME `BlockId` on
6177 /// both edges, so there genuinely is a suspend-free path from `cur` to
6178 /// the guarded body in the built `ProcessCfg` (a purely structural,
6179 /// compile-time fact -- codegen decides which edge is taken at
6180 /// simulation runtime by reusing the exact same generic branch-condition
6181 /// lowering `if`/`while` already use, `docs/design/process.md`'s own
6182 /// `is_definitely_true` reduction list explicitly includes "wait()
6183 /// condition" alongside "if"/"while" for this reason, PR-069's
6184 /// `Terminator`/`SuspendPoint` "cross-check against
6185 /// contains_suspend_trigger" extension policy). `resume`'s own entry is
6186 /// therefore executed either way (already-true fallthrough, or a genuine
6187 /// resume from the scheduler after the `Suspend` fires) -- the
6188 /// resume-prologue re-check that makes a spurious wake re-suspend
6189 /// without running the body is CG-115's own job, entirely on the
6190 /// codegen side (`vj-codegen::process_lower::compile_iff_guard`);
6191 /// this crate never evaluates `expr` itself (no runtime state reaches
6192 /// this AST-only builder).
6193 fn walk_wait(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
6194 let cond = stmt.condition().ok_or_else(|| {
6195 LoweringError::malformed("Wait statement with no condition expression")
6196 })?;
6197 let resume = self.alloc_block();
6198 let suspend_blk = self.alloc_block();
6199 self.close(
6200 cur,
6201 Terminator::Branch {
6202 cond,
6203 then_blk: resume,
6204 else_blk: suspend_blk,
6205 },
6206 );
6207 self.close(
6208 suspend_blk,
6209 Terminator::Suspend {
6210 point: SuspendPoint::Level(cond),
6211 resume_blk: resume,
6212 },
6213 );
6214 let guarded = stmt
6215 .body()
6216 .ok_or_else(|| LoweringError::malformed("Wait statement with no guarded body"))?;
6217 self.walk_stmt(guarded, resume)
6218 }
6219
6220 /// `-> ev;` / `->> ev;` / `->> #d ev;` / `->> @(ctl) ev;` (LRM 1800-2023
6221 /// 15.5.1, 15.5.2). An `EventTrigger` with no timing, or with a `#d`
6222 /// delay, never suspends the issuer -- it is one ordinary leaf (codegen
6223 /// stages the delayed trigger as a nonblocking update). One with an event
6224 /// control (`->> @(ctl) ev;`) forks a detached one-shot process that
6225 /// waits on `ctl` and then triggers, so the issuer continues at once
6226 /// (`spawn_detached_trigger`).
6227 fn walk_event_trigger(
6228 &mut self,
6229 stmt: Statement<'d>,
6230 cur: BlockId,
6231 ) -> Result<BlockId, LoweringError> {
6232 let detached = std::mem::take(&mut self.detached_trigger);
6233 let Some(timing_node) = stmt.timing() else {
6234 self.push(cur, stmt);
6235 return Ok(cur);
6236 };
6237 let tc = timing_node.as_timing_control().ok_or_else(|| {
6238 LoweringError::malformed("EventTrigger timing is not a TimingControl node")
6239 })?;
6240 if tc.kind() == TimingControlKind::Delay {
6241 // `->> #d e;` never blocks the issuer: the trigger is staged as a
6242 // nonblocking update `d` ticks out (codegen reads the delay).
6243 self.push(cur, stmt);
6244 return Ok(cur);
6245 }
6246 if !detached {
6247 // `->> @(ctl) e;`: the issuer continues at once; a detached
6248 // process waits for `ctl`, then stages the trigger.
6249 return self.spawn_detached_trigger(stmt, cur, ProcessLoopKind::OneShot);
6250 }
6251 let point = self.suspend_point(tc, None)?;
6252 let resume = self.alloc_block();
6253 self.close(
6254 cur,
6255 Terminator::Suspend {
6256 point,
6257 resume_blk: resume,
6258 },
6259 );
6260 self.push(resume, stmt);
6261 Ok(resume)
6262 }
6263
6264 /// Forks the detached one-shot process of `->> @(ctl) e;` (see
6265 /// [`Self::detached_trigger`]) and continues the issuer past it.
6266 fn spawn_detached_trigger(
6267 &mut self,
6268 stmt: Statement<'d>,
6269 cur: BlockId,
6270 loop_kind: ProcessLoopKind,
6271 ) -> Result<BlockId, LoweringError> {
6272 let mut branch = build_cfg_in(
6273 &stmt,
6274 loop_kind,
6275 None,
6276 self.default_clocking,
6277 None,
6278 self.checker_clock,
6279 Inherited {
6280 this_var: self.this_stack.last().copied(),
6281 ref_aliases: self.ref_stack.clone(),
6282 global_clock: self.global_clock,
6283 detached_trigger: true,
6284 forced_extern: None,
6285 rec_outer: self.rec_outer.clone(),
6286 fork_memo: Some(self.fork_memo.clone()),
6287 rec: None,
6288 rec_up: Vec::new(),
6289 known_fork_labels: Vec::new(),
6290 ancestor_blocks: Vec::new(),
6291 },
6292 self.virtual_tasks.clone(),
6293 )?;
6294 branch.captured = captured_vars(&branch);
6295 branch
6296 .captured
6297 .retain(|c| !self.ref_stack.iter().any(|(f, _)| f.uid() == c.uid()));
6298 if let Some(t) = self.this_stack.last()
6299 && !branch.captured.iter().any(|c| c.uid() == t.uid())
6300 {
6301 branch.captured.push(*t);
6302 }
6303 let join_blk = self.alloc_block();
6304 self.close(
6305 cur,
6306 Terminator::ForkJoin {
6307 kind: StatementBlockKind::JoinNone,
6308 branches: vec![branch],
6309 join_blk,
6310 },
6311 );
6312 Ok(join_blk)
6313 }
6314
6315 /// `$async$and$array(mem, in, out);` and its seven siblings (LRM 1800-2023
6316 /// 20.16.1): the logic array is evaluated at the call, and again whenever
6317 /// an input term or any word of the personality memory changes. The
6318 /// issuer evaluates once (the call is an ordinary leaf) and forks a
6319 /// detached looping process that evaluates, waits on the operands' read
6320 /// set, and re-runs the same statement on each wake (the
6321 /// `detached_trigger` flag marks that process's own copy, so it does not
6322 /// fork again).
6323 fn walk_async_pla(
6324 &mut self,
6325 stmt: Statement<'d>,
6326 cur: BlockId,
6327 ) -> Result<BlockId, LoweringError> {
6328 let detached = std::mem::take(&mut self.detached_trigger);
6329 if !detached {
6330 self.push(cur, stmt);
6331 return self.spawn_detached_trigger(stmt, cur, ProcessLoopKind::Looping);
6332 }
6333 // Evaluate first, then wait: an input that changed between the fork
6334 // and this process's first run (same time step) is not missed.
6335 let reads = async_pla_read_set(&stmt);
6336 let resume = self.alloc_block();
6337 self.push(cur, stmt);
6338 self.close(
6339 cur,
6340 Terminator::Suspend {
6341 point: SuspendPoint::ImplicitComb(reads),
6342 resume_blk: resume,
6343 },
6344 );
6345 Ok(resume)
6346 }
6347
6348 /// A call statement `stmt` to the interface `extern` method `targets`
6349 /// (LRM 25.7), when the call needs the CFG: it runs the implementation
6350 /// inline (one), or as the branches of a `fork`...`join` (an `extern
6351 /// forkjoin` task with several, 25.7.5), or reports the run-time error of
6352 /// a forkjoin task nothing implements and returns with no effect.
6353 /// `None` when the call is an ordinary leaf (every implementation runs
6354 /// without suspending: the code generator calls them in turn).
6355 fn walk_extern_call(
6356 &mut self,
6357 stmt: Statement<'d>,
6358 call: Expression<'d>,
6359 targets: &crate::netlist::itf::ExternTargets<'d>,
6360 cur: BlockId,
6361 ) -> Result<Option<BlockId>, LoweringError> {
6362 // One branch of an enclosing fork: this implementation only.
6363 if let Some(imp) = self.forced_extern.take() {
6364 return self.inline_task_call(call, imp, cur).map(Some);
6365 }
6366 if targets.impls.is_empty() {
6367 let join = self.alloc_block();
6368 self.close(
6369 cur,
6370 Terminator::Switch {
6371 on: SwitchOn::NoTarget {
6372 what: format!(
6373 "no module connected to the interface implements extern forkjoin \
6374 task `{}`; the call has no effect",
6375 targets.proto.name()
6376 ),
6377 },
6378 arms: Vec::new(),
6379 default_blk: join,
6380 report: Some(call.source_range().start),
6381 },
6382 );
6383 return Ok(Some(join));
6384 }
6385 if !targets.impls.iter().any(subroutine_can_suspend) {
6386 return Ok(None);
6387 }
6388 if targets.impls.len() == 1 {
6389 return self.inline_task_call(call, targets.impls[0], cur).map(Some);
6390 }
6391 // Every implementation as a branch of one `fork`...`join` (the call
6392 // returns when all have).
6393 let mut branches = Vec::with_capacity(targets.impls.len());
6394 for imp in &targets.impls {
6395 let mut branch = build_cfg_in(
6396 &stmt,
6397 ProcessLoopKind::OneShot,
6398 None,
6399 self.default_clocking,
6400 None,
6401 self.checker_clock,
6402 Inherited {
6403 this_var: self.this_stack.last().copied(),
6404 ref_aliases: self.ref_stack.clone(),
6405 global_clock: self.global_clock,
6406 detached_trigger: false,
6407 forced_extern: Some(*imp),
6408 rec_outer: self.rec_outer.clone(),
6409 fork_memo: Some(self.fork_memo.clone()),
6410 rec: None,
6411 rec_up: Vec::new(),
6412 known_fork_labels: self.fork_branch_names.clone(),
6413 ancestor_blocks: self.ancestor_blocks.clone(),
6414 },
6415 self.virtual_tasks.clone(),
6416 )?;
6417 branch.captured = captured_vars(&branch);
6418 branch
6419 .captured
6420 .retain(|c| !self.ref_stack.iter().any(|(f, _)| f.uid() == c.uid()));
6421 if let Some(t) = self.this_stack.last()
6422 && !branch.captured.iter().any(|c| c.uid() == t.uid())
6423 {
6424 branch.captured.push(*t);
6425 }
6426 branches.push(branch);
6427 }
6428 let join_blk = self.alloc_block();
6429 self.close(
6430 cur,
6431 Terminator::ForkJoin {
6432 kind: StatementBlockKind::JoinAll,
6433 branches,
6434 join_blk,
6435 },
6436 );
6437 Ok(Some(join_blk))
6438 }
6439
6440 /// PR-045 (Task 1): `while (cond) body;`. `header` evaluates `cond`
6441 /// every iteration (re-checked, not cached) and branches to `body` or
6442 /// `exit`; `body`'s own exit closes the back edge to `header`; the real
6443 /// entry from `cur` is `header` (the condition is checked before the
6444 /// first iteration).
6445 fn walk_while_loop(
6446 &mut self,
6447 stmt: Statement<'d>,
6448 cur: BlockId,
6449 ) -> Result<BlockId, LoweringError> {
6450 let cond = stmt
6451 .condition()
6452 .ok_or_else(|| LoweringError::malformed("WhileLoop with no condition expression"))?;
6453 let body_stmt = stmt
6454 .body()
6455 .ok_or_else(|| LoweringError::malformed("WhileLoop with no body statement"))?;
6456
6457 let header = self.alloc_block();
6458 let body = self.alloc_block();
6459 let exit = self.alloc_block();
6460 // 38.36.1.2: before the condition, on every iteration.
6461 self.probe(header, stmt);
6462 self.close(
6463 cur,
6464 Terminator::Loop {
6465 header,
6466 body,
6467 exit,
6468 entry: header,
6469 },
6470 );
6471 self.close(
6472 header,
6473 Terminator::Branch {
6474 cond,
6475 then_blk: body,
6476 else_blk: exit,
6477 },
6478 );
6479 let body_exit = self.walk_loop_body(body_stmt, body, header, exit)?;
6480 self.close(body_exit, Terminator::Fallthrough(header));
6481 Ok(exit)
6482 }
6483
6484 /// PR-045 (Task 1): `do body; while (cond);`. Unlike `while`, `body`
6485 /// always runs once *before* any check -- the real entry from `cur` is
6486 /// `body` itself (`Terminator::Loop::entry`), and `header` (the
6487 /// post-body recheck, reached only via `body`'s own back edge) branches
6488 /// back to `body` or out to `exit`.
6489 fn walk_do_while_loop(
6490 &mut self,
6491 stmt: Statement<'d>,
6492 cur: BlockId,
6493 ) -> Result<BlockId, LoweringError> {
6494 let cond = stmt
6495 .condition()
6496 .ok_or_else(|| LoweringError::malformed("DoWhileLoop with no condition expression"))?;
6497 let body_stmt = stmt
6498 .body()
6499 .ok_or_else(|| LoweringError::malformed("DoWhileLoop with no body statement"))?;
6500
6501 let header = self.alloc_block();
6502 let body = self.alloc_block();
6503 let exit = self.alloc_block();
6504 self.close(
6505 cur,
6506 Terminator::Loop {
6507 header,
6508 body,
6509 exit,
6510 entry: body,
6511 },
6512 );
6513 self.close(
6514 header,
6515 Terminator::Branch {
6516 cond,
6517 then_blk: body,
6518 else_blk: exit,
6519 },
6520 );
6521 let body_exit = self.walk_loop_body(body_stmt, body, header, exit)?;
6522 self.close(body_exit, Terminator::Fallthrough(header));
6523 Ok(exit)
6524 }
6525
6526 /// PR-045 (Task 1): `forever body;`. No condition exists at all (LRM:
6527 /// unconditional) -- `header` unconditionally falls through to `body`,
6528 /// never to `exit`, so `exit` is allocated (every `Terminator::Loop`
6529 /// always names one, per PR-045's literal shape) but unreachable by any
6530 /// statically-analyzable path; only `disable`/`$finish` end a `forever`
6531 /// loop in practice, neither of which this function needs to wire.
6532 fn walk_forever_loop(
6533 &mut self,
6534 stmt: Statement<'d>,
6535 cur: BlockId,
6536 ) -> Result<BlockId, LoweringError> {
6537 let body_stmt = stmt
6538 .body()
6539 .ok_or_else(|| LoweringError::malformed("ForeverLoop with no body statement"))?;
6540
6541 let header = self.alloc_block();
6542 let body = self.alloc_block();
6543 let exit = self.alloc_block();
6544 // 38.36.1.2: when first encountered and on every iteration.
6545 self.probe(header, stmt);
6546 self.close(
6547 cur,
6548 Terminator::Loop {
6549 header,
6550 body,
6551 exit,
6552 entry: header,
6553 },
6554 );
6555 self.close(header, Terminator::Fallthrough(body));
6556 let body_exit = self.walk_loop_body(body_stmt, body, header, exit)?;
6557 self.close(body_exit, Terminator::Fallthrough(header));
6558 Ok(exit)
6559 }
6560
6561 /// PR-045 (Task 1): `for (init; cond; step) body;`. The declaration-form
6562 /// initializer (`for (int i = 0; ...)`) is a separate `VariableDeclaration`
6563 /// statement slang hoists *before* this `ForLoop` node in an implicit
6564 /// wrapper `Block` -- already handled for free by the ordinary `List`/
6565 /// `Block` walk, so this function never sees it. The pre-declared-var
6566 /// form (`for (i = 0; ...)`, `for_loop_initializers()` non-empty) is a
6567 /// named `LoweringError`: those initializers are bare `Expression`s, and
6568 /// (like `for_loop_steps()`, see `BasicBlock::step_exprs`'s doc comment)
6569 /// this crate has no way to run a bare pre-loop `Expression` as an
6570 /// ordinary leaf effect without fabricating a `Statement` node it does
6571 /// not have -- a real, if narrow, gap recorded in this task's
6572 /// deviations, not a silent drop. The condition (`stop expression`) is
6573 /// re-checked every iteration in `header`; the step expressions run via
6574 /// `step_exprs` on a dedicated `step` block between `body` and the back
6575 /// edge to `header`.
6576 fn walk_for_loop(
6577 &mut self,
6578 stmt: Statement<'d>,
6579 cur: BlockId,
6580 ) -> Result<BlockId, LoweringError> {
6581 // The classic Verilog form `for (i = 0; ...)` (pre-declared `i`):
6582 // its bare initializer expressions run as `step_exprs` on `cur`
6583 // itself -- after `cur`'s own statements, before the jump into
6584 // `header` -- the same bare-expression mechanism the loop's step
6585 // expressions use below.
6586 // 38.36.1.2: before any of the control expressions (the initializers
6587 // run as `cur`'s step expressions, after its statements) ...
6588 self.probe(cur, stmt);
6589 for init in stmt.for_loop_initializers() {
6590 self.push_step_expr(cur, init);
6591 }
6592 // `for (;;)`: an omitted stop expression is always true (LRM
6593 // 1800-2023 12.7.1), so `header` falls through to `body` unconditionally.
6594 let cond = stmt.condition();
6595 let body_stmt = stmt
6596 .body()
6597 .ok_or_else(|| LoweringError::malformed("ForLoop with no body statement"))?;
6598 let steps = stmt.for_loop_steps();
6599
6600 let header = self.alloc_block();
6601 let body = self.alloc_block();
6602 let exit = self.alloc_block();
6603 self.close(
6604 cur,
6605 Terminator::Loop {
6606 header,
6607 body,
6608 exit,
6609 entry: header,
6610 },
6611 );
6612 match cond {
6613 Some(cond) => self.close(
6614 header,
6615 Terminator::Branch {
6616 cond,
6617 then_blk: body,
6618 else_blk: exit,
6619 },
6620 ),
6621 None => self.close(header, Terminator::Fallthrough(body)),
6622 }
6623 let step = self.alloc_block();
6624 let body_exit = self.walk_loop_body(body_stmt, body, step, exit)?;
6625 self.close(body_exit, Terminator::Fallthrough(step));
6626 // ... and on every iteration before the incremental statement.
6627 self.probe(step, stmt);
6628 for s in steps {
6629 self.push_step_expr(step, s);
6630 }
6631 self.close(step, Terminator::Fallthrough(header));
6632 Ok(exit)
6633 }
6634
6635 /// PR-045: `foreach (arr[i, j, ...]) body;`. A dimension's bound is a
6636 /// static `ConstantRange` (`Statement::foreach_loop_dims`), never an
6637 /// `Expression`, so each named dimension is a counted loop on the
6638 /// `repeat` machinery (`RepeatCount::Const` elements, a `RepeatCheck`
6639 /// header), its index variable set by `ArgBinding::Const`/`Step`. The
6640 /// body is lowered once. A dynamically-sized dimension (`range() ==
6641 /// None`) is a named `LoweringError`.
6642 fn walk_foreach_loop(
6643 &mut self,
6644 stmt: Statement<'d>,
6645 cur: BlockId,
6646 ) -> Result<BlockId, LoweringError> {
6647 // Over a queue, dynamic or associative array: a leaf statement
6648 // codegen lowers as one loop (`stmt_shared::lower_collection_
6649 // foreach`) when its body cannot suspend.
6650 let over_collection = stmt
6651 .expressions()
6652 .first()
6653 .and_then(|a| a.expr_type())
6654 .is_some_and(|t| crate::state::storage::is_collection(&t));
6655 // A suspending body over a collection nested in a collection or in a
6656 // fixed-size array (`int a[2][]`, `int q[$][$]`): `walk_nested_foreach`.
6657 let nested_dyn = stmt
6658 .foreach_loop_dims()
6659 .iter()
6660 .skip(usize::from(over_collection))
6661 .any(|d| d.range().is_none());
6662 // A `return` in the body of an inlined task has to jump out of the
6663 // CFG's task body (LRM 1800-2023 13.4.1), which a leaf loop cannot do.
6664 let returns = escapes(stmt.as_sem_node(), false, !self.return_targets.is_empty());
6665 if nested_dyn && (contains_suspend_trigger(stmt.as_sem_node()) || returns) {
6666 return self.walk_nested_foreach(stmt, cur);
6667 }
6668 if over_collection {
6669 if contains_suspend_trigger(stmt.as_sem_node()) || returns {
6670 return self.walk_collection_foreach(stmt, cur);
6671 }
6672 self.push(cur, stmt);
6673 return Ok(cur);
6674 }
6675 // A fixed-size array of queues / dynamic arrays (`int a[3][]`): one
6676 // leaf loop when the body cannot suspend (`stmt_shared::
6677 // lower_fixed_outer_foreach`).
6678 if stmt.foreach_loop_dims().iter().any(|d| d.range().is_none())
6679 && !contains_suspend_trigger(stmt.as_sem_node())
6680 && !returns
6681 {
6682 self.push(cur, stmt);
6683 return Ok(cur);
6684 }
6685 let dims = stmt.foreach_loop_dims();
6686 let body_stmt = stmt
6687 .body()
6688 .ok_or_else(|| LoweringError::malformed("ForeachLoop with no body statement"))?;
6689 // One counted loop per named dimension, outermost first; a dimension
6690 // whose loop variable is omitted (`foreach (a[i, ])`) is not
6691 // iterated (LRM 1800-2023 §12.7.3).
6692 let mut loops = Vec::new();
6693 for d in &dims {
6694 let Some(var) = d.loop_var() else {
6695 continue;
6696 };
6697 let range = d.range().ok_or_else(|| {
6698 LoweringError::unsupported(
6699 "foreach over a dynamically-sized dimension (a dynamic array, queue or \
6700 associative array)",
6701 )
6702 })?;
6703 loops.push((var, i64::from(range.left), i64::from(range.right)));
6704 }
6705 // Every index omitted: no dimension is iterated, the body never runs.
6706 if loops.is_empty() {
6707 return Ok(cur);
6708 }
6709 self.walk_foreach_nest(&loops, body_stmt, cur, None)
6710 }
6711
6712 /// A suspending `foreach` where a queue / dynamic array is nested in a
6713 /// fixed-size array or in another collection (`int a[2][3][]`, `int
6714 /// d[][]`, `int q[2][$]`). One counted loop per named dimension,
6715 /// outermost first (LRM 1800-2023 12.7.3); a fixed dimension counts its
6716 /// static range, a collection dimension counts the size of the element
6717 /// reached through the enclosing indices ([`RepeatCount::NestedSize`]),
6718 /// read when that loop is entered. An omitted index in front of a
6719 /// collection dimension and an associative array in the chain are named
6720 /// errors (the leaf lowering refuses the same shapes).
6721 fn walk_nested_foreach(
6722 &mut self,
6723 stmt: Statement<'d>,
6724 cur: BlockId,
6725 ) -> Result<BlockId, LoweringError> {
6726 let arr = stmt.expressions()[0];
6727 let body_stmt = stmt
6728 .body()
6729 .ok_or_else(|| LoweringError::malformed("ForeachLoop with no body statement"))?;
6730 let dims = stmt.foreach_loop_dims();
6731 let last_dyn = dims.iter().rposition(|d| d.range().is_none());
6732 // Whether each dimension is an associative array (its variable is a
6733 // key): peel the array type one dimension at a time.
6734 let mut ty = arr.expr_type();
6735 let mut assoc_dims = Vec::new();
6736 for _ in &dims {
6737 assoc_dims.push(ty.as_ref().is_some_and(|t| t.is_associative_array()));
6738 ty = ty.and_then(|t| t.element_type());
6739 }
6740 let mut levels = Vec::new();
6741 for (k, d) in dims.iter().enumerate() {
6742 let Some(var) = d.loop_var() else {
6743 if last_dyn.is_some_and(|l| l > k) {
6744 return Err(LoweringError::unsupported(
6745 "foreach whose body waits with an omitted index in front of a nested \
6746 queue, dynamic-array or associative-array dimension",
6747 ));
6748 }
6749 continue;
6750 };
6751 let fixed = d.range().map(|r| (r.left, r.right));
6752 levels.push(NestStep {
6753 var,
6754 fixed,
6755 assoc: assoc_dims[k],
6756 });
6757 }
6758 self.walk_nest_levels(&levels, &mut Vec::new(), arr, body_stmt, cur, None)
6759 }
6760
6761 /// The loops of [`Self::walk_nested_foreach`], `levels[0]` outermost;
6762 /// `prefix` holds the enclosing levels. `break` leaves the whole nest.
6763 fn walk_nest_levels(
6764 &mut self,
6765 levels: &[NestStep<'d>],
6766 prefix: &mut Vec<NestStep<'d>>,
6767 arr: Expression<'d>,
6768 body_stmt: Statement<'d>,
6769 cur: BlockId,
6770 outer_exit: Option<BlockId>,
6771 ) -> Result<BlockId, LoweringError> {
6772 let Some(&step_info) = levels.first() else {
6773 return self.walk_stmt(body_stmt, cur);
6774 };
6775 let var = step_info.var;
6776 let entry = self.alloc_block();
6777 self.close(cur, Terminator::Fallthrough(entry));
6778 let counter = self.alloc_repeat_counter();
6779 let (count, first, by) = match step_info.fixed {
6780 Some((left, right)) => {
6781 let by: i64 = if left <= right { 1 } else { -1 };
6782 let n = (i64::from(left) - i64::from(right)).unsigned_abs() + 1;
6783 (RepeatCount::Const(n), i64::from(left) - by, by)
6784 }
6785 None if prefix.is_empty() => (RepeatCount::Size(arr), -1, 1),
6786 None => (
6787 RepeatCount::NestedSize {
6788 arr,
6789 path: prefix.clone(),
6790 },
6791 -1,
6792 1,
6793 ),
6794 };
6795 self.set_repeat_init(entry, counter, count);
6796 let header = self.alloc_block();
6797 let body = self.alloc_block();
6798 let exit = self.alloc_block();
6799 self.inlined_locals.push(var);
6800 if step_info.assoc {
6801 // Keys walk first/next, like a top-level associative array.
6802 self.blocks[entry.0 as usize]
6803 .bindings
6804 .push(ArgBinding::AssocKey {
6805 var,
6806 arr,
6807 path: prefix.clone(),
6808 next: false,
6809 });
6810 } else {
6811 self.blocks[entry.0 as usize]
6812 .bindings
6813 .push(ArgBinding::Const { var, value: first });
6814 self.blocks[header.0 as usize]
6815 .bindings
6816 .push(ArgBinding::Step { var, by });
6817 }
6818 self.close(
6819 entry,
6820 Terminator::Loop {
6821 header,
6822 body,
6823 exit,
6824 entry: header,
6825 },
6826 );
6827 self.close(
6828 header,
6829 Terminator::RepeatCheck {
6830 counter,
6831 then_blk: body,
6832 else_blk: exit,
6833 },
6834 );
6835 let latch = self.alloc_block();
6836 self.set_repeat_decrement(latch, counter);
6837 if step_info.assoc {
6838 self.blocks[latch.0 as usize]
6839 .bindings
6840 .push(ArgBinding::AssocKey {
6841 var,
6842 arr,
6843 path: prefix.clone(),
6844 next: true,
6845 });
6846 }
6847 self.close(latch, Terminator::Fallthrough(header));
6848 let outer = outer_exit.unwrap_or(exit);
6849 prefix.push(step_info);
6850 let body_exit = if levels.len() == 1 {
6851 self.walk_loop_body(body_stmt, body, latch, outer)?
6852 } else {
6853 self.walk_nest_levels(&levels[1..], prefix, arr, body_stmt, body, Some(outer))?
6854 };
6855 prefix.pop();
6856 self.close(body_exit, Terminator::Fallthrough(latch));
6857 Ok(exit)
6858 }
6859
6860 /// A `foreach` over a queue, dynamic or associative array whose body
6861 /// waits: a counted loop over the size the array has at loop entry (the
6862 /// `repeat` machinery, as for a fixed-size array). The index walks up
6863 /// from 0; an associative array's key starts at its first key and moves
6864 /// to the next one after each iteration (`first`/`next`, which is how
6865 /// LRM 1800-2023 §12.7.3 orders the keys).
6866 fn walk_collection_foreach(
6867 &mut self,
6868 stmt: Statement<'d>,
6869 cur: BlockId,
6870 ) -> Result<BlockId, LoweringError> {
6871 let arr = stmt.expressions()[0];
6872 let assoc = arr.expr_type().is_some_and(|t| t.is_associative_array());
6873 let dims = stmt.foreach_loop_dims();
6874 let body_stmt = stmt
6875 .body()
6876 .ok_or_else(|| LoweringError::malformed("ForeachLoop with no body statement"))?;
6877 // The inner dimensions are fixed-size ranges (`int d[][3]`): a
6878 // collection's elements are never themselves collections, so only
6879 // the outermost dimension is dynamic (LRM 1800-2023 §12.7.3).
6880 let mut rest = Vec::new();
6881 for d in dims.iter().skip(1) {
6882 let Some(v) = d.loop_var() else {
6883 continue;
6884 };
6885 let range = d.range().ok_or_else(|| {
6886 LoweringError::unsupported(
6887 "foreach over a dimension of a queue or dynamic array's element that is \
6888 itself a queue, dynamic or associative array",
6889 )
6890 })?;
6891 rest.push((v, i64::from(range.left), i64::from(range.right)));
6892 }
6893 let Some(var) = dims.first().and_then(|d| d.loop_var()) else {
6894 // Outermost index omitted: only the named inner dimensions run.
6895 return if rest.is_empty() {
6896 Ok(cur)
6897 } else {
6898 self.walk_foreach_nest(&rest, body_stmt, cur, None)
6899 };
6900 };
6901 let entry = self.alloc_block();
6902 self.close(cur, Terminator::Fallthrough(entry));
6903 let counter = self.alloc_repeat_counter();
6904 self.set_repeat_init(entry, counter, RepeatCount::Size(arr));
6905 let header = self.alloc_block();
6906 let body = self.alloc_block();
6907 let exit = self.alloc_block();
6908 self.inlined_locals.push(var);
6909 if assoc {
6910 self.blocks[entry.0 as usize]
6911 .bindings
6912 .push(ArgBinding::AssocKey {
6913 var,
6914 arr,
6915 path: Vec::new(),
6916 next: false,
6917 });
6918 } else {
6919 self.blocks[entry.0 as usize]
6920 .bindings
6921 .push(ArgBinding::Const { var, value: -1 });
6922 self.blocks[header.0 as usize]
6923 .bindings
6924 .push(ArgBinding::Step { var, by: 1 });
6925 }
6926 self.close(
6927 entry,
6928 Terminator::Loop {
6929 header,
6930 body,
6931 exit,
6932 entry: header,
6933 },
6934 );
6935 self.close(
6936 header,
6937 Terminator::RepeatCheck {
6938 counter,
6939 then_blk: body,
6940 else_blk: exit,
6941 },
6942 );
6943 let latch = self.alloc_block();
6944 self.set_repeat_decrement(latch, counter);
6945 if assoc {
6946 self.blocks[latch.0 as usize]
6947 .bindings
6948 .push(ArgBinding::AssocKey {
6949 var,
6950 arr,
6951 path: Vec::new(),
6952 next: true,
6953 });
6954 }
6955 self.close(latch, Terminator::Fallthrough(header));
6956 let body_exit = if rest.is_empty() {
6957 self.walk_loop_body(body_stmt, body, latch, exit)?
6958 } else {
6959 self.walk_foreach_nest(&rest, body_stmt, body, Some(exit))?
6960 };
6961 self.close(body_exit, Terminator::Fallthrough(latch));
6962 Ok(exit)
6963 }
6964
6965 /// The counted loops of [`Self::walk_foreach_loop`], `loops[0]`
6966 /// outermost: each is the `repeat` machinery counting the dimension's
6967 /// elements, its index variable walking from the left bound to the right
6968 /// bound (it starts one step before `left`; the header advances it
6969 /// before each iteration's count check).
6970 ///
6971 /// The nest is one loop to `break`/`continue` (LRM 1800-2023 §12.7.3,
6972 /// Verilator agrees): `continue` advances to the next element (the
6973 /// innermost dimension's latch) and `break` leaves the whole nest
6974 /// (`outer_exit`, the outermost dimension's exit).
6975 fn walk_foreach_nest(
6976 &mut self,
6977 loops: &[(Symbol<'d>, i64, i64)],
6978 body_stmt: Statement<'d>,
6979 cur: BlockId,
6980 outer_exit: Option<BlockId>,
6981 ) -> Result<BlockId, LoweringError> {
6982 let Some(&(var, left, right)) = loops.first() else {
6983 return self.walk_stmt(body_stmt, cur);
6984 };
6985 let step: i64 = if left <= right { 1 } else { -1 };
6986 let count = (left - right).unsigned_abs() + 1;
6987 // A fresh block for this loop's entry: `cur` may already carry
6988 // another counted loop's `repeat_init` (an enclosing foreach body).
6989 let entry = self.alloc_block();
6990 self.close(cur, Terminator::Fallthrough(entry));
6991 let counter = self.alloc_repeat_counter();
6992 self.set_repeat_init(entry, counter, RepeatCount::Const(count));
6993 let header = self.alloc_block();
6994 let body = self.alloc_block();
6995 let exit = self.alloc_block();
6996 self.inlined_locals.push(var);
6997 self.blocks[entry.0 as usize]
6998 .bindings
6999 .push(ArgBinding::Const {
7000 var,
7001 value: left - step,
7002 });
7003 self.blocks[header.0 as usize]
7004 .bindings
7005 .push(ArgBinding::Step { var, by: step });
7006 self.close(
7007 entry,
7008 Terminator::Loop {
7009 header,
7010 body,
7011 exit,
7012 entry: header,
7013 },
7014 );
7015 self.close(
7016 header,
7017 Terminator::RepeatCheck {
7018 counter,
7019 then_blk: body,
7020 else_blk: exit,
7021 },
7022 );
7023 // The latch counts the iteration down; `continue` comes here too.
7024 let latch = self.alloc_block();
7025 self.set_repeat_decrement(latch, counter);
7026 self.close(latch, Terminator::Fallthrough(header));
7027 let outer = outer_exit.unwrap_or(exit);
7028 let body_exit = if loops.len() == 1 {
7029 self.walk_loop_body(body_stmt, body, latch, outer)?
7030 } else {
7031 self.walk_foreach_nest(&loops[1..], body_stmt, body, Some(outer))?
7032 };
7033 self.close(body_exit, Terminator::Fallthrough(latch));
7034 Ok(exit)
7035 }
7036
7037 /// R-run Task 8 (PR-045/PR-006): `repeat(N) body;` when `body` contains
7038 /// a suspend point (the gate `walk_stmt`'s own dispatch already checked
7039 /// before calling this function). Mechanically similar to
7040 /// [`Self::walk_while_loop`] -- `header`'s own terminator carries the
7041 /// real per-iteration decision, `body`'s own terminator closes the back
7042 /// edge, `entry` is `header` (the counter is checked before the first
7043 /// iteration too, LRM: `repeat(0)` runs zero times) -- except the
7044 /// decision is [`Terminator::RepeatCheck`] against a compiler-
7045 /// synthesized [`RepeatCounterId`], not a re-evaluated user `Branch`
7046 /// condition, and `cur` (this loop's own entry block) additionally
7047 /// records a [`BasicBlock::repeat_init`] evaluating the count expression
7048 /// exactly once before ever reaching `header`. The loop body is walked
7049 /// recursively via the ordinary [`Self::walk_stmt`] entry point, so any
7050 /// suspend terminator nested inside it composes for free -- exactly the
7051 /// same reason `disable` "composes correctly across suspend boundaries
7052 /// for free" per `docs/design/process.md` §2, and the same recursive
7053 /// shape every other loop-walker in this file already uses.
7054 fn walk_repeat_loop(
7055 &mut self,
7056 stmt: Statement<'d>,
7057 cur: BlockId,
7058 ) -> Result<BlockId, LoweringError> {
7059 // R-run Task 4's `stmt_shared::lower_repeat_loop` already established
7060 // (and this file's own Task 4-era doc comments confirm) that
7061 // `RepeatLoop::condition()` is the count expression, not a boolean --
7062 // slang has no separate accessor for it, this AST node just reuses
7063 // the `condition()` grammar slot for `repeat`'s own count operand.
7064 let count_expr = stmt
7065 .condition()
7066 .ok_or_else(|| LoweringError::malformed("RepeatLoop with no count expression"))?;
7067 let body_stmt = stmt
7068 .body()
7069 .ok_or_else(|| LoweringError::malformed("RepeatLoop with no body statement"))?;
7070
7071 let counter = self.alloc_repeat_counter();
7072 self.set_repeat_init(cur, counter, RepeatCount::Expr(count_expr));
7073
7074 let header = self.alloc_block();
7075 let body = self.alloc_block();
7076 let exit = self.alloc_block();
7077 // 38.36.1.2: when first encountered and at every iteration (so one
7078 // more time than the loop body runs, the last one finding the count
7079 // exhausted -- the same shape the leaf lowering has).
7080 self.probe(header, stmt);
7081 self.close(
7082 cur,
7083 Terminator::Loop {
7084 header,
7085 body,
7086 exit,
7087 entry: header,
7088 },
7089 );
7090 self.close(
7091 header,
7092 Terminator::RepeatCheck {
7093 counter,
7094 then_blk: body,
7095 else_blk: exit,
7096 },
7097 );
7098 // The latch counts the iteration down; `continue` comes here too.
7099 let latch = self.alloc_block();
7100 self.set_repeat_decrement(latch, counter);
7101 self.close(latch, Terminator::Fallthrough(header));
7102 let body_exit = self.walk_loop_body(body_stmt, body, latch, exit)?;
7103 self.close(body_exit, Terminator::Fallthrough(latch));
7104 Ok(exit)
7105 }
7106}
7107
7108/// Whether `node` (a statement kept as one straight-line leaf) contains a
7109/// jump out of itself into the CFG around it: a `break`/`continue` not
7110/// inside a loop of its own (when `in_loop`, an enclosing CFG loop exists)
7111/// or a `return` (when `in_task`, the statement is part of an inlined task
7112/// body).
7113fn escapes(node: SemNode<'_>, in_loop: bool, in_task: bool) -> bool {
7114 let mut stack = vec![(node, false)];
7115 while let Some((n, nested_loop)) = stack.pop() {
7116 let mut inner = nested_loop;
7117 if let Some(s) = n.as_statement() {
7118 let kind = s.kind();
7119 if matches!(kind, StatementKind::Break | StatementKind::Continue)
7120 && in_loop
7121 && !nested_loop
7122 {
7123 return true;
7124 }
7125 if kind == StatementKind::Return && in_task {
7126 return true;
7127 }
7128 if matches!(
7129 kind,
7130 StatementKind::ForLoop
7131 | StatementKind::WhileLoop
7132 | StatementKind::DoWhileLoop
7133 | StatementKind::ForeverLoop
7134 | StatementKind::ForeachLoop
7135 | StatementKind::RepeatLoop
7136 ) {
7137 inner = true;
7138 }
7139 }
7140 stack.extend(n.children().into_iter().map(|c| (c, inner)));
7141 }
7142 false
7143}
7144
7145/// Whether `stmt` is a statement a VPI `cbStmt` callback fires for once, just
7146/// before it executes (IEEE 1800-2023 Table 38-6): the blocks, `if`, `case`,
7147/// `wait`, the procedural assignments / `force` / `release` / `disable` /
7148/// event triggers, task and system-task calls, and the delay and event
7149/// controls. This mirrors the statements the VPI model gives an object of
7150/// those types. The loops (`while`/`for`/`repeat`/`forever`) fire per
7151/// iteration instead and are not covered here.
7152pub fn is_entry_probe_kind(stmt: &Statement<'_>) -> bool {
7153 match stmt.kind() {
7154 StatementKind::Block
7155 | StatementKind::Conditional
7156 | StatementKind::Case
7157 | StatementKind::PatternCase
7158 | StatementKind::Wait
7159 | StatementKind::Disable
7160 | StatementKind::EventTrigger
7161 | StatementKind::ProceduralAssign
7162 | StatementKind::ProceduralDeassign => true,
7163 StatementKind::ExpressionStatement => stmt.expr().is_some_and(|e| {
7164 matches!(
7165 e.kind(),
7166 ExpressionKind::Assignment | ExpressionKind::Call | ExpressionKind::UnaryOp
7167 )
7168 }),
7169 StatementKind::Timed => {
7170 stmt.timing_control().map(|t| t.kind()) != Some(TimingControlKind::RepeatedEvent)
7171 }
7172 // Not in Table 38-6, or a loop (probed per iteration): no entry probe.
7173 StatementKind::Invalid
7174 | StatementKind::Empty
7175 | StatementKind::List
7176 | StatementKind::VariableDeclaration
7177 | StatementKind::Return
7178 | StatementKind::Continue
7179 | StatementKind::Break
7180 | StatementKind::DisableFork
7181 | StatementKind::ForLoop
7182 | StatementKind::RepeatLoop
7183 | StatementKind::ForeachLoop
7184 | StatementKind::WhileLoop
7185 | StatementKind::DoWhileLoop
7186 | StatementKind::ForeverLoop
7187 | StatementKind::ImmediateAssertion
7188 | StatementKind::ConcurrentAssertion
7189 | StatementKind::WaitFork
7190 | StatementKind::WaitOrder
7191 | StatementKind::RandCase
7192 | StatementKind::RandSequence
7193 | StatementKind::ProceduralChecker => false,
7194 other => unreachable!("statement kind {other:?} is newer than this classification"),
7195 }
7196}
7197
7198thread_local! {
7199 /// The text of each source file of the design (as it was parsed, so
7200 /// offsets of a [`sv_lang::SourceLoc`] index it), which `disable_label`
7201 /// reads a `disable` statement's target from, by file name. One file
7202 /// registered without a name ([`set_source_text`]) answers for every
7203 /// location.
7204 static DISABLE_SOURCE: std::cell::RefCell<Vec<(Option<String>, std::rc::Rc<str>)>> =
7205 const { std::cell::RefCell::new(Vec::new()) };
7206}
7207
7208/// Registers the text of the source file statements are elaborated from,
7209/// for `disable label;` (see [`disable_label`]); `None` clears it.
7210pub fn set_source_text(text: Option<&str>) {
7211 DISABLE_SOURCE.with(|s| {
7212 *s.borrow_mut() = text
7213 .map(|t| (None, std::rc::Rc::from(t)))
7214 .into_iter()
7215 .collect();
7216 });
7217 scan_disable_names(text.into_iter());
7218}
7219
7220thread_local! {
7221 /// What the registered source text names (see [`scan_disable_names`]):
7222 /// every `disable <name>;` target, and every task name.
7223 static DISABLE_NAMES: std::cell::RefCell<(
7224 std::collections::HashSet<String>,
7225 std::collections::HashSet<String>,
7226 )> = std::cell::RefCell::new(Default::default());
7227}
7228
7229/// Records which names a `disable <name>;` of the design targets, and which
7230/// names are tasks, from the registered source text (the binding cannot read a
7231/// `disable` target, see the module doc). Over-approximate on purpose (a word
7232/// in a comment counts): a name here only makes the builder keep a scope exit
7233/// for it ([`CfgBuilder::scope_chain`]) or accept it as a task of the design.
7234fn scan_disable_names<'a>(texts: impl Iterator<Item = &'a str>) {
7235 let mut labels = std::collections::HashSet::new();
7236 let mut tasks = std::collections::HashSet::new();
7237 let mut fn_labels = std::collections::HashSet::new();
7238 let ident = |c: u8| c == b'_' || c == b'$' || c.is_ascii_alphanumeric();
7239 for text in texts {
7240 let b = text.as_bytes();
7241 let word_at = |mut i: usize| -> Option<(&str, usize)> {
7242 while i < b.len() && b[i].is_ascii_whitespace() {
7243 i += 1;
7244 }
7245 let start = i;
7246 while i < b.len() && ident(b[i]) {
7247 i += 1;
7248 }
7249 (i > start).then(|| (&text[start..i], i))
7250 };
7251 let mut i = 0;
7252 // Inside a `function` body (a DPI prototype or `pure`/`extern`
7253 // declaration has none, and ends at its `;`).
7254 let (mut in_fn, mut proto) = (false, false);
7255 while i < b.len() {
7256 if !ident(b[i]) {
7257 if b[i] == b';' {
7258 proto = false;
7259 }
7260 i += 1;
7261 continue;
7262 }
7263 let start = i;
7264 while i < b.len() && ident(b[i]) {
7265 i += 1;
7266 }
7267 let boundary = start == 0 || (b[start - 1] != b'.' && b[start - 1] != b'`');
7268 match &text[start..i] {
7269 "import" | "export" | "extern" | "pure" if boundary => proto = true,
7270 "function" if boundary => in_fn = !proto,
7271 "endfunction" if boundary => in_fn = false,
7272 "disable" if boundary => {
7273 // `disable a.b.c;` (9.6.2) names its last segment.
7274 // A generate-block index (`g[0].bk`) is part of a segment.
7275 let skip_idx = |mut e: usize| -> usize {
7276 loop {
7277 let t = text[e..].trim_start();
7278 let lead = text[e..].len() - t.len();
7279 let Some(inner) = t.strip_prefix('[') else {
7280 return e;
7281 };
7282 match inner.find(']') {
7283 Some(c)
7284 if inner[..c].trim().bytes().all(|d| d.is_ascii_digit()) =>
7285 {
7286 e += lead + 1 + c + 1;
7287 }
7288 _ => return e,
7289 }
7290 }
7291 };
7292 if let Some((mut name, e0)) = word_at(i)
7293 && name != "fork"
7294 {
7295 let mut end = skip_idx(e0);
7296 while text[end..].trim_start().starts_with('.')
7297 && let Some((next, e)) = word_at(
7298 end + (text[end..].len() - text[end..].trim_start().len()) + 1,
7299 )
7300 {
7301 name = next;
7302 end = skip_idx(e);
7303 }
7304 if text[end..].trim_start().starts_with(';') {
7305 labels.insert(name.to_string());
7306 if in_fn {
7307 fn_labels.insert(name.to_string());
7308 }
7309 // The whole dotted path too ([`CfgBuilder::name_path`]).
7310 let full: String = text[i..end]
7311 .chars()
7312 .filter(|c| !c.is_ascii_whitespace())
7313 .collect();
7314 // Every dotted suffix too: `disable u1.main.inner;`
7315 // names the block `inner` of `main` by the path
7316 // below the instance (9.6.2).
7317 let mut rest = full.as_str();
7318 while let Some((_, tail)) = rest.split_once('.') {
7319 if tail.contains('.') {
7320 labels.insert(tail.to_string());
7321 }
7322 rest = tail;
7323 }
7324 if full.contains('.') {
7325 labels.insert(full);
7326 }
7327 }
7328 }
7329 }
7330 "task" if boundary => {
7331 if let Some((mut name, end)) = word_at(i) {
7332 if matches!(name, "automatic" | "static")
7333 && let Some((n, _)) = word_at(end)
7334 {
7335 name = n;
7336 }
7337 tasks.insert(name.to_string());
7338 }
7339 }
7340 _ => {}
7341 }
7342 }
7343 }
7344 DISABLE_NAMES.with(|n| *n.borrow_mut() = (labels, tasks));
7345 FN_DISABLE_LABELS.with(|n| *n.borrow_mut() = fn_labels);
7346}
7347
7348thread_local! {
7349 /// The targets of the `disable <name>;` statements written inside a
7350 /// `function` body (see [`scan_disable_names`]).
7351 static FN_DISABLE_LABELS: std::cell::RefCell<std::collections::HashSet<String>> =
7352 std::cell::RefCell::new(Default::default());
7353}
7354
7355/// Whether the design has a `disable <name>;` inside a function body: such a
7356/// function can end a block of its caller (LRM 9.6.2, 35.9), which the call
7357/// sites check for ([`SwitchOn::FnDisable`]). Designs without one pay nothing.
7358pub fn has_function_disables() -> bool {
7359 FN_DISABLE_LABELS.with(|n| !n.borrow().is_empty())
7360}
7361
7362/// Whether a function body of the design has `disable <label>;`.
7363pub fn is_function_disable_label(label: &str) -> bool {
7364 FN_DISABLE_LABELS.with(|n| n.borrow().contains(label))
7365}
7366
7367/// Whether some `disable` statement of the design names `label`.
7368fn is_disabled_somewhere(label: &str) -> bool {
7369 DISABLE_NAMES.with(|n| n.borrow().0.contains(label))
7370}
7371
7372/// Every dotted `disable a.b.c;` target (and dotted suffix of one) the design
7373/// writes, sorted.
7374pub fn dotted_disable_labels() -> Vec<String> {
7375 let mut v: Vec<String> = DISABLE_NAMES.with(|n| {
7376 n.borrow()
7377 .0
7378 .iter()
7379 .filter(|l| l.contains('.'))
7380 .cloned()
7381 .collect()
7382 });
7383 v.sort();
7384 v
7385}
7386
7387/// Whether `label` is the name of a task of the design.
7388pub fn is_task_name(label: &str) -> bool {
7389 DISABLE_NAMES.with(|n| n.borrow().1.contains(label))
7390}
7391
7392/// Registers the text of every source file of a multi-file design, each
7393/// under the file name its locations resolve to ([`set_source_session`]).
7394pub fn set_source_texts(files: Vec<(String, String)>) {
7395 scan_disable_names(files.iter().map(|(_, t)| t.as_str()));
7396 DISABLE_SOURCE.with(|s| {
7397 *s.borrow_mut() = files
7398 .into_iter()
7399 .map(|(name, text)| (Some(name), std::rc::Rc::from(text)))
7400 .collect();
7401 });
7402}
7403
7404/// Whether two file names denote the same file (equal, or the same file on
7405/// disk).
7406fn same_file(a: &str, b: &str) -> bool {
7407 a == b
7408 || std::fs::canonicalize(a)
7409 .ok()
7410 .is_some_and(|x| std::fs::canonicalize(b).ok() == Some(x))
7411}
7412
7413/// The registered text holding `at`: the unnamed one if that is what was
7414/// registered, else the named file `at` resolves to (through the session
7415/// registered by [`set_source_session`]).
7416fn source_text_at(at: sv_lang::SourceLoc) -> Option<std::rc::Rc<str>> {
7417 let files = DISABLE_SOURCE.with(|s| s.borrow().clone());
7418 if let [(None, text)] = files.as_slice() {
7419 return Some(text.clone());
7420 }
7421 let file = SOURCE_SESSION
7422 .with(|s| s.borrow().as_ref().and_then(|se| se.resolve_location(at)))?
7423 .file;
7424 files
7425 .into_iter()
7426 .find(|(n, _)| n.as_deref().is_some_and(|n| same_file(n, &file)))
7427 .map(|(_, text)| text)
7428}
7429
7430thread_local! {
7431 /// The main source file's name as given (`$info`'s `file:line`).
7432 static SOURCE_NAME: std::cell::RefCell<Option<String>> =
7433 const { std::cell::RefCell::new(None) };
7434}
7435
7436/// Registers the main source file's name (see [`source_location`]).
7437pub fn set_source_name(name: Option<&str>) {
7438 SOURCE_NAME.with(|s| *s.borrow_mut() = name.map(str::to_owned));
7439}
7440
7441/// The source text of `span`, from the file text registered by
7442/// [`set_source_text`] / [`set_source_texts`] (`None` if that file's text is
7443/// not registered, e.g. a span in an `` `include `` d file).
7444pub fn source_text_range(span: sv_lang::SourceSpan) -> Option<String> {
7445 source_text_at(span.start)?
7446 .get(span.start.offset as usize..span.end.offset as usize)
7447 .map(str::to_owned)
7448}
7449
7450thread_local! {
7451 /// The session the design was parsed with: resolves a source location
7452 /// to its file and line (included files and `` `line `` directives
7453 /// included).
7454 static SOURCE_SESSION: std::cell::RefCell<Option<sv_lang::Session>> =
7455 const { std::cell::RefCell::new(None) };
7456}
7457
7458/// Registers the session the design was parsed with (see
7459/// [`source_location`]); `None` clears it.
7460pub fn set_source_session(session: Option<sv_lang::Session>) {
7461 SOURCE_SESSION.with(|s| *s.borrow_mut() = session);
7462}
7463
7464/// `file:line` of `loc`, as slang's source manager reports it (honoring
7465/// `` `include `` and `` `line ``) through the session registered by
7466/// [`set_source_session`] -- the main source file under the name it was
7467/// given ([`set_source_name`]); without a session, counted in the main
7468/// source file registered by [`set_source_name`]/[`set_source_text`].
7469pub fn source_location(loc: sv_lang::SourceLoc) -> Option<String> {
7470 if let Some(at) = SOURCE_SESSION.with(|s| {
7471 s.borrow()
7472 .as_ref()
7473 .and_then(|session| session.resolve_location(loc))
7474 }) {
7475 let main = SOURCE_NAME.with(|s| s.borrow().clone());
7476 let file = match main {
7477 Some(m) if same_file(&at.file, &m) => m,
7478 _ => at.file,
7479 };
7480 return Some(format!("{file}:{}", at.line));
7481 }
7482 let name = SOURCE_NAME.with(|s| s.borrow().clone())?;
7483 let text = source_text_at(loc)?;
7484 let head = text.get(..(loc.offset as usize).min(text.len()))?;
7485 let line = head.bytes().filter(|&b| b == b'\n').count() + 1;
7486 Some(format!("{name}:{line}"))
7487}
7488
7489/// The segments of a hierarchical `disable a.b.c;` target (LRM 9.6.2), read
7490/// from the statement's source text like [`disable_label`]; `None` for a
7491/// simple or unreadable target.
7492pub fn disable_path(stmt: &Statement<'_>) -> Option<Vec<String>> {
7493 let text = source_text_range(stmt.source_range())?;
7494 let rest = text.trim().strip_prefix("disable")?;
7495 if !rest.starts_with(char::is_whitespace) {
7496 return None;
7497 }
7498 let label: String = rest
7499 .trim()
7500 .trim_end_matches(';')
7501 .chars()
7502 .filter(|c| !c.is_whitespace())
7503 .collect();
7504 let segs: Vec<String> = label.split('.').map(str::to_string).collect();
7505 // A generate-block segment carries its index (`g[0]`).
7506 let ident = |t: &str| {
7507 let t = match t.find('[') {
7508 Some(b)
7509 if t.ends_with(']')
7510 && t[b + 1..t.len() - 1].bytes().all(|d| d.is_ascii_digit())
7511 && b + 2 < t.len() =>
7512 {
7513 &t[..b]
7514 }
7515 Some(_) => return false,
7516 None => t,
7517 };
7518 !t.is_empty()
7519 && t.chars().enumerate().all(|(i, c)| {
7520 c == '_' || c.is_ascii_alphabetic() || (i > 0 && (c.is_ascii_digit() || c == '$'))
7521 })
7522 };
7523 (segs.len() > 1 && segs.iter().all(|t| ident(t))).then_some(segs)
7524}
7525
7526/// The name `disable` acts on in a statement the CFG builder kept: the plain
7527/// label, or the last segment of a hierarchical `a.b.c` the builder resolved to
7528/// a block of another process ([`disable_path`]).
7529pub fn disable_leaf_label(stmt: &Statement<'_>) -> Result<String, LoweringError> {
7530 match disable_path(stmt).and_then(|p| p.last().cloned()) {
7531 Some(leaf) => Ok(leaf),
7532 None => disable_label(stmt),
7533 }
7534}
7535
7536/// The target name of a `disable label;` statement. The pinned sv-lang
7537/// binding exposes no `DisableStatement::target` (see the module doc), so
7538/// it is read from the statement's own source span in the text registered
7539/// by [`set_source_text`] -- accepted only when that span reads exactly
7540/// `disable <identifier> ;`, so a span into another file (an `` `include ``)
7541/// is a named error, never a misread label.
7542pub fn disable_label(stmt: &Statement<'_>) -> Result<String, LoweringError> {
7543 let span = stmt.source_range();
7544 let text = source_text_range(span);
7545 let unreadable = || {
7546 LoweringError::unsupported(
7547 "disable statement whose target cannot be read (no registered source text, or \
7548 the statement is not in a registered source file)",
7549 )
7550 };
7551 let text = text.ok_or_else(unreadable)?;
7552 let rest = text.trim().strip_prefix("disable").ok_or_else(unreadable)?;
7553 let label = rest.trim().trim_end_matches(';').trim();
7554 let ident = !label.is_empty()
7555 && label.chars().enumerate().all(|(i, c)| {
7556 c == '_' || c.is_ascii_alphabetic() || (i > 0 && (c.is_ascii_digit() || c == '$'))
7557 });
7558 if !ident || !rest.starts_with(char::is_whitespace) {
7559 let hier = if label.contains('.') {
7560 "; disabling a task or interface method by hierarchical name would have to \
7561 end every activation of it in other processes (IEEE 1800-2023 9.6.2)"
7562 } else {
7563 ""
7564 };
7565 return Err(LoweringError::unsupported(format!(
7566 "disable of `{label}` (only a simple block name is supported{hier})"
7567 )));
7568 }
7569 Ok(label.to_string())
7570}
7571
7572/// Whether `node`'s subtree contains a `disable label;` statement, in itself
7573/// or in the body of a task it calls (an inlined task's `disable` may target
7574/// the caller's block, LRM 9.6.2).
7575fn contains_disable_through_calls(node: SemNode<'_>) -> bool {
7576 let mut visiting = std::collections::HashSet::new();
7577 let mut stack = vec![node];
7578 while let Some(n) = stack.pop() {
7579 if n.as_statement()
7580 .is_some_and(|s| s.kind() == StatementKind::Disable)
7581 {
7582 return true;
7583 }
7584 if let Some(e) = n.as_expression()
7585 && let Some(callee) = user_task_callee(&e)
7586 && let Some(body) = proc_body(&callee)
7587 && visiting.insert(callee.uid())
7588 {
7589 stack.push(body.as_sem_node());
7590 }
7591 stack.extend(live_children(&n));
7592 }
7593 false
7594}
7595
7596/// Whether `node`'s subtree holds a `fork`/`join*` block, in itself or in the
7597/// body of a task it calls.
7598fn contains_fork_through_calls(node: SemNode<'_>) -> bool {
7599 let mut visiting = std::collections::HashSet::new();
7600 let mut stack = vec![node];
7601 while let Some(n) = stack.pop() {
7602 if n.as_statement().is_some_and(|s| {
7603 matches!(
7604 s.block_kind(),
7605 Some(
7606 StatementBlockKind::JoinAll
7607 | StatementBlockKind::JoinAny
7608 | StatementBlockKind::JoinNone
7609 )
7610 )
7611 }) {
7612 return true;
7613 }
7614 if let Some(e) = n.as_expression()
7615 && let Some(callee) = user_task_callee(&e)
7616 && let Some(body) = proc_body(&callee)
7617 && visiting.insert(callee.uid())
7618 {
7619 stack.push(body.as_sem_node());
7620 }
7621 stack.extend(live_children(&n));
7622 }
7623 false
7624}
7625
7626/// Every `StatementKind` variant [`CfgBuilder::walk_stmt`] treats as ordinary
7627/// straight-line code (PR-006's own acceptance text) -- i.e. every variant
7628/// that is not one of the 5 explicitly-named statement kinds
7629/// (`List`/`Block`/`Conditional`/`Timed`/the 5 suspend-rejecting kinds) it
7630/// handles directly. Kept as a `matches!` (not a literal `match`, see
7631/// `walk_stmt`'s doc comment) enumerating all 22 remaining variants of
7632/// today's pinned `StatementKind::COUNT == 32`.
7633/// Task 1 removed `ForLoop`/`WhileLoop`/`DoWhileLoop`/`ForeverLoop`/
7634/// `ForeachLoop`/`Disable` from this set. The five loop kinds now dispatch
7635/// to dedicated handlers (PR-045's `Terminator::Loop`) instead of being
7636/// treated as opaque leaves; `Disable` also gets a dedicated `walk_stmt` arm
7637/// (a jump to its named block's exit -- see this file's module doc comment's
7638/// `disable label;` section), never the "push as ordinary code" treatment
7639/// this function's callers give everything below. `RepeatLoop` (a plain statement-level `repeat(N)
7640/// stmt;`, distinct from the `RepeatedEvent` *timing control*) stays
7641/// ordinary -- PR-045's own requirement text names exactly `for`/`while`/
7642/// `do-while`/`forever`/`foreach`, not `repeat`.
7643fn is_ordinary_statement_kind(kind: StatementKind) -> bool {
7644 matches!(
7645 kind,
7646 StatementKind::Empty
7647 | StatementKind::ExpressionStatement
7648 | StatementKind::VariableDeclaration
7649 | StatementKind::Return
7650 | StatementKind::Continue
7651 | StatementKind::Break
7652 | StatementKind::Case
7653 | StatementKind::PatternCase
7654 | StatementKind::RepeatLoop
7655 | StatementKind::ImmediateAssertion
7656 | StatementKind::ConcurrentAssertion
7657 | StatementKind::ProceduralAssign
7658 | StatementKind::ProceduralDeassign
7659 | StatementKind::RandCase
7660 | StatementKind::RandSequence
7661 | StatementKind::ProceduralChecker
7662 )
7663}
7664
7665/// R-run Task 1: `true` iff `stmt` (already known to be a `StatementKind::
7666/// ExpressionStatement`) is a bare `$finish`/`$finish(n)`/`$stop`/`$fatal`
7667/// system-call TASK statement -- the exact same `ExpressionKind::Call` +
7668/// `is_system_call() == Some(true)` + `system_subroutine().known_name_id()`
7669/// recognition `vj-codegen::process_lower::lower_system_call_statement`
7670/// already uses for its own dispatch (mirrored, not re-derived differently:
7671/// both call sites must agree on what counts as one of these three calls).
7672/// `$fatal`'s own optional finish_number/message arguments and `$stop`'s own
7673/// interactive-debugger-break runtime semantics are unaffected by this CFG-
7674/// level check -- it only ever answers "does the calling process's own
7675/// control flow end here", never anything about the call's own argument
7676/// list or its eventual codegen lowering (`$stop`/`$fatal` still lower to a
7677/// named `LoweringError` in `process_lower.rs` today, exactly as before this
7678/// task; this predicate firing for them one file earlier changes nothing
7679/// observable about that until a later task actually lowers them).
7680fn is_finish_or_stop_call(stmt: &Statement<'_>) -> bool {
7681 let Some(e) = stmt.expr() else {
7682 return false;
7683 };
7684 if e.kind() != ExpressionKind::Call || e.is_system_call() != Some(true) {
7685 return false;
7686 }
7687 let Some(sub) = e.system_subroutine() else {
7688 return false;
7689 };
7690 matches!(
7691 sub.known_name_id(),
7692 KnownSystemName::Finish | KnownSystemName::Stop | KnownSystemName::Fatal
7693 )
7694}
7695
7696/// The `unique`/`unique0`/`priority` qualifier on an `if` or `case` statement
7697/// (LRM 1800-2023 §12.4.2, §12.5.3), `None` for an unqualified statement or
7698/// any other kind. A `case ... matches` ([`StatementKind::PatternCase`])
7699/// carries its qualifier under its own accessor.
7700pub fn violation_check(stmt: &Statement<'_>) -> Option<UniquePriorityCheck> {
7701 let check = if stmt.kind() == StatementKind::PatternCase {
7702 stmt.pattern_case_check()
7703 } else {
7704 stmt.check()
7705 };
7706 match check {
7707 Some(UniquePriorityCheck::None) | None => None,
7708 Some(check) => Some(check),
7709 }
7710}
7711
7712/// The `if` / `else if` series a `unique`/`unique0`/`priority` qualifier on
7713/// the first `if` applies to (§12.4.2: "the entire series of if-else-if
7714/// conditions"): the condition and then-statement of each link, in source
7715/// order, and the final `else` statement if there is one. A nested
7716/// `else if` carries no qualifier of its own (the grammar has none); an
7717/// `else` holding a qualified `if` of its own is the final `else` and a
7718/// separate statement.
7719pub struct IfChain<'d> {
7720 /// The `if` statement of each link (its predicate is `links[i]
7721 /// .conditions()`: plain, `&&&`-chained, or pattern-matching, §12.6).
7722 pub links: Vec<Statement<'d>>,
7723 pub thens: Vec<Statement<'d>>,
7724 pub otherwise: Option<Statement<'d>>,
7725}
7726
7727/// Peels `Invalid` wrappers off `stmt` (IEEE 1800-2023 12.4/12.5).
7728///
7729/// slang suppresses the error of a construct inside a statically dead `if`
7730/// or `case` arm (e.g. a reversed range select `v[7:8]` of a descending
7731/// vector under a false parameter) but demotes every enclosing statement to
7732/// `Invalid`. Such wrappers nest (an `Invalid` body wraps a `Block` that wraps
7733/// an `Invalid` wrapping the `Conditional`), so this unwraps recursively via
7734/// `Statement::invalid_child`. An `Invalid` with no recoverable child is
7735/// returned unchanged, so the caller's loud "Invalid statement" refusal still
7736/// fires for it.
7737pub fn unwrap_invalid<'d>(mut stmt: Statement<'d>) -> Statement<'d> {
7738 while stmt.kind() == StatementKind::Invalid {
7739 match stmt.invalid_child() {
7740 Some(child) => stmt = child,
7741 None => break,
7742 }
7743 }
7744 stmt
7745}
7746
7747/// The child statements of a `List`/`Block`, with each statement slang
7748/// demoted to `Invalid` (a statically dead `if`/`case` arm it could not
7749/// bind) unwrapped to the real one. `Statement::statements` skips `Invalid`
7750/// children, which would silently drop the live statement (LRM 12.4/12.5).
7751pub fn live_statements<'d>(stmt: &Statement<'d>) -> Vec<Statement<'d>> {
7752 stmt.as_sem_node()
7753 .children_with_invalid()
7754 .into_iter()
7755 .filter_map(|n| n.as_statement())
7756 .map(unwrap_invalid)
7757 .collect()
7758}
7759
7760/// The children of `node` for a whole-subtree walk, with each `Invalid`
7761/// statement or expression (slang's demotion of a construct inside a
7762/// statically dead `if`/`case` arm it could not bind, LRM 12.4/12.5) replaced
7763/// by the real node it wraps. `SemNode::children` skips `Invalid` children,
7764/// so a call or read inside the live arm of such a statement would otherwise
7765/// be invisible to every discovery walk (called-subroutine discovery, write
7766/// sets, unsupported-construct scans).
7767pub fn live_children<'d>(node: &SemNode<'d>) -> Vec<SemNode<'d>> {
7768 node.children_with_invalid()
7769 .into_iter()
7770 .map(|n| {
7771 if let Some(s) = n.as_statement() {
7772 return unwrap_invalid(s).as_sem_node();
7773 }
7774 if let Some(mut e) = n.as_expression() {
7775 while e.kind() == ExpressionKind::Invalid
7776 && let Some(c) = e.invalid_child()
7777 {
7778 e = c;
7779 }
7780 return e.as_sem_node();
7781 }
7782 n
7783 })
7784 .collect()
7785}
7786
7787/// The body statement of a procedural block with any `Invalid` wrappers from
7788/// a statically dead arm peeled off (see [`unwrap_invalid`]). Every pass that
7789/// inspects a process's head timing control, read set or statements must go
7790/// through this rather than `Symbol::body`, so they all see the same live
7791/// body (otherwise an `always @*` loses its sensitivity).
7792pub fn proc_body<'d>(block: &Symbol<'d>) -> Option<Statement<'d>> {
7793 block.body().map(unwrap_invalid)
7794}
7795
7796/// The `StatementBlock` scopes whose locals `stmt` (a `Block`) declares.
7797///
7798/// Normally that is `Statement::block_symbol()`. slang never attaches the
7799/// symbol to a block that failed to bind (`Statement::bindBlock` returns the
7800/// `Invalid` wrapper before setting `blockSymbol`), so a `begin`/`end` that
7801/// holds a statically dead `if`/`case` arm it could not bind (LRM 12.4/12.5)
7802/// is recovered through `invalid_child` with no symbol, and its locals would
7803/// be invisible. For such a block the scopes are recovered from the variables
7804/// the live subtree references: the `StatementBlock` parents of its
7805/// `NamedValue` symbols. That can include an enclosing or nested block's
7806/// scope, which is harmless for the union-style local collections that call
7807/// this; a local no live statement references is not recovered (it has no
7808/// observable use).
7809pub fn block_scopes<'d>(stmt: &Statement<'d>) -> Vec<Symbol<'d>> {
7810 if let Some(bs) = stmt.block_symbol() {
7811 return vec![bs];
7812 }
7813 let node = stmt.as_sem_node();
7814 if stmt.kind() != StatementKind::Block || !contains_invalid(node) {
7815 return Vec::new();
7816 }
7817 let mut out: Vec<Symbol<'d>> = Vec::new();
7818 let mut seen = HashSet::new();
7819 let mut stack = vec![node];
7820 while let Some(n) = stack.pop() {
7821 if let Some(e) = n.as_expression()
7822 && crate::symid::is_value_name(e.kind())
7823 && let Some(sym) = e.referenced_symbol()
7824 && let Some(parent) = sym.parent()
7825 && parent.kind() == SymbolKind::StatementBlock
7826 && seen.insert(parent.uid())
7827 {
7828 out.push(parent);
7829 }
7830 stack.extend(live_children(&n));
7831 }
7832 out
7833}
7834
7835/// The name of a `begin`/`end` or `fork`/`join` block statement (LRM 9.3.4),
7836/// empty when unnamed.
7837///
7838/// Normally `Statement::block_symbol()`'s name. A block that failed to bind
7839/// (a statically dead arm slang could not bind, LRM 12.4) carries no symbol,
7840/// so its name would be lost and `disable blk` (LRM 9.6.2) refused. For such
7841/// a block the name is read back from the statement's own source span in the
7842/// text registered by [`set_source_text`]: either the `begin : name` form or
7843/// a `name : begin` statement label. Anything else reads as unnamed.
7844pub fn block_name(stmt: &Statement<'_>) -> String {
7845 if let Some(bs) = stmt.block_symbol() {
7846 return bs.name().to_string();
7847 }
7848 if stmt.kind() != StatementKind::Block || !contains_invalid(stmt.as_sem_node()) {
7849 return String::new();
7850 }
7851 let Some(text) = source_text_range(stmt.source_range()) else {
7852 return String::new();
7853 };
7854 let ident = |s: &str| -> Option<(String, String)> {
7855 let s = s.trim_start();
7856 let n = s
7857 .char_indices()
7858 .take_while(|&(i, c)| {
7859 c == '_' || c.is_ascii_alphabetic() || (i > 0 && (c.is_ascii_digit() || c == '$'))
7860 })
7861 .count();
7862 (n > 0).then(|| (s[..n].to_string(), s[n..].to_string()))
7863 };
7864 let Some((first, rest)) = ident(&text) else {
7865 return String::new();
7866 };
7867 if matches!(first.as_str(), "begin" | "fork") {
7868 // `begin : name`; a bare `begin` followed by a statement is unnamed.
7869 return match rest.trim_start().strip_prefix(':').and_then(ident) {
7870 Some((name, _)) => name,
7871 None => String::new(),
7872 };
7873 }
7874 // `name : begin`.
7875 match rest.trim_start().strip_prefix(':').and_then(ident) {
7876 Some((kw, _)) if matches!(kw.as_str(), "begin" | "fork") => first,
7877 _ => String::new(),
7878 }
7879}
7880
7881/// Whether any node under `node` is an `Invalid` statement or expression.
7882fn contains_invalid(node: SemNode<'_>) -> bool {
7883 let is_invalid = node
7884 .as_statement()
7885 .is_some_and(|s| s.kind() == StatementKind::Invalid)
7886 || node
7887 .as_expression()
7888 .is_some_and(|e| e.kind() == ExpressionKind::Invalid);
7889 is_invalid
7890 || node
7891 .children_with_invalid()
7892 .into_iter()
7893 .any(contains_invalid)
7894}
7895
7896/// For a plain `case` whose selector and labels are all compile-time
7897/// constants and whose arms include an `Invalid` one (a statically dead arm
7898/// slang could not bind, LRM 12.5): `Some(arm)` names the one statement that
7899/// can ever run (`None` inside: no arm matches and there is no `default`).
7900/// `None` for any case this does not apply to (non-constant, `casez`/`casex`/
7901/// `inside`, a call in a label, or no `Invalid` arm), which lowers as usual.
7902pub fn constant_case_live_arm<'d>(stmt: &Statement<'d>) -> Option<Option<Statement<'d>>> {
7903 fn has_call(e: Expression<'_>) -> bool {
7904 e.kind() == ExpressionKind::Call || e.operands().into_iter().any(has_call)
7905 }
7906 if stmt.kind() != StatementKind::Case
7907 || stmt.case_condition() != Some(sv_lang::CaseStatementCondition::Normal)
7908 || violation_check(stmt).is_some()
7909 {
7910 return None;
7911 }
7912 let items = stmt.case_items();
7913 let default = stmt.default_case();
7914 if !items
7915 .iter()
7916 .map(|i| i.stmt())
7917 .chain(default)
7918 .any(|a| contains_invalid(a.as_sem_node()))
7919 {
7920 return None;
7921 }
7922 let sel = stmt.expr()?;
7923 if has_call(sel) {
7924 return None;
7925 }
7926 let sv = sel.constant_value()?.as_i64()?;
7927 for item in &items {
7928 for label in item.expressions() {
7929 if has_call(label) {
7930 return None;
7931 }
7932 if label.constant_value()?.as_i64()? == sv {
7933 return Some(Some(item.stmt()));
7934 }
7935 }
7936 }
7937 Some(default)
7938}
7939
7940/// The fixed truth of a plain `if`'s predicate when it is a compile-time
7941/// constant with no function call in it (`if (PARAM > 0)`): `Some(taken)`
7942/// names the one arm that can ever run, so the other need not be lowered
7943/// (LRM 1800-2023 12.4; the dead arm may be legal yet unlowerable once
7944/// its constants fold, e.g. an out-of-range part-select, 11.5.1). A call is
7945/// excluded because the evaluator would run it at compile time, skipping
7946/// its side effects. `None` for any other `if`.
7947pub fn constant_if_arm(stmt: &Statement<'_>) -> Option<bool> {
7948 fn has_call(e: Expression<'_>) -> bool {
7949 e.kind() == ExpressionKind::Call || e.operands().into_iter().any(has_call)
7950 }
7951 let conditions = stmt.conditions();
7952 if stmt.kind() != StatementKind::Conditional
7953 || conditions.len() != 1
7954 || conditions[0].pattern().is_some()
7955 || violation_check(stmt).is_some()
7956 {
7957 return None;
7958 }
7959 let cond = conditions[0].expr();
7960 if has_call(cond) {
7961 return None;
7962 }
7963 cond.constant_value()
7964 .and_then(|c| c.as_i64())
7965 .map(|c| c != 0)
7966}
7967
7968/// Whether `stmt` is an `if` whose predicate is not a single plain
7969/// expression: a pattern match (`e matches p`) or an `&&&` chain (§12.6.1).
7970pub fn is_clause_if(stmt: &Statement<'_>) -> bool {
7971 if stmt.kind() != StatementKind::Conditional {
7972 return false;
7973 }
7974 let conditions = stmt.conditions();
7975 conditions.len() != 1 || conditions[0].pattern().is_some()
7976}
7977
7978/// The links an `if` dispatches over: the whole `else if` series of a
7979/// `unique`/`unique0`/`priority` `if`, else the one `if` alone (its `else`,
7980/// if any, is the `otherwise` statement).
7981pub fn if_series<'d>(head: Statement<'d>) -> Result<IfChain<'d>, &'static str> {
7982 if violation_check(&head).is_some() {
7983 return if_chain(head);
7984 }
7985 Ok(IfChain {
7986 links: vec![head],
7987 thens: vec![head.then_branch().ok_or("an `if` with no then-branch")?],
7988 otherwise: head.else_branch(),
7989 })
7990}
7991
7992/// Collects the [`IfChain`] of the `if` statement `head`.
7993pub fn if_chain<'d>(head: Statement<'d>) -> Result<IfChain<'d>, &'static str> {
7994 let mut chain = IfChain {
7995 links: Vec::new(),
7996 thens: Vec::new(),
7997 otherwise: None,
7998 };
7999 let mut link = head;
8000 loop {
8001 chain.links.push(link);
8002 chain
8003 .thens
8004 .push(link.then_branch().ok_or("an `if` with no then-branch")?);
8005 match link.else_branch() {
8006 Some(next)
8007 if next.kind() == StatementKind::Conditional
8008 && violation_check(&next).is_none() =>
8009 {
8010 link = next;
8011 }
8012 other => {
8013 chain.otherwise = other;
8014 return Ok(chain);
8015 }
8016 }
8017 }
8018}
8019
8020fn first_expression_child<'d>(tc: &TimingControl<'d>) -> Option<Expression<'d>> {
8021 tc.children().into_iter().find_map(|n| n.as_expression())
8022}
8023
8024/// Task 6 (CG-081): the AST-derived read-set of `body` -- every distinct
8025/// (by referenced-symbol identity) `NamedValue` expression `body`'s subtree
8026/// reads, excluding a plain `NamedValue` LHS of a top-level `Assignment`
8027/// (mirrors `vj-codegen::process_lower::external_refs_in_segment`'s
8028/// identical "read vs write position" split, at the granularity of one
8029/// guarded statement rather than a whole `Segment`).
8030///
8031/// **Deliberately AST-only, not `AnalyzedProcedure::
8032/// implicit_event_read_sets()`:** `sv-lang` exposes exactly this oracle
8033/// (`slang::analysis::AnalyzedProcedure::getImplicitEventReadSets`), but it
8034/// needs a live `sv_lang::Analysis` handle, and no `build_cfg` call site in
8035/// this tree (`vj::run`, `aot.rs`, `pipeline.rs`, `frontend.rs`, this
8036/// crate's own `liveness.rs` tests) threads one through today -- adding that
8037/// parameter would be a signature change to `build_cfg` reaching every one
8038/// of those call sites, including `vj::run.rs`, which this wave's
8039/// binding rules forbid risking (wave-B-runtime is concurrently landing
8040/// there). This AST-only walk is a real, if narrower, substitute: an
8041/// over-approximation (a name reached only through a path this walk cannot
8042/// prove is write-only) is a safe, LRM-permitted implicit-sensitivity
8043/// superset (an extra spurious wake, never a missed one) -- never a
8044/// correctness bug the way under-approximating would be. See this file's
8045/// module doc comment's Task 6 section and this task's own deviations for
8046/// the full accounting.
8047fn implicit_comb_read_set<'d>(body: &Statement<'d>) -> Vec<Expression<'d>> {
8048 let mut order = Vec::new();
8049 let mut seen = HashSet::new();
8050 let mut stack = vec![body.as_sem_node()];
8051 while let Some(n) = stack.pop() {
8052 // A subtree slang demoted to `Invalid` inside a statically dead arm
8053 // is invisible to `children`; recover it (its reads are a harmless
8054 // superset of the live ones, LRM 9.4.2.2).
8055 if let Some(st) = n.as_statement()
8056 && st.kind() == StatementKind::Invalid
8057 {
8058 if let Some(c) = st.invalid_child() {
8059 stack.push(c.as_sem_node());
8060 }
8061 continue;
8062 }
8063 if let Some(e) = n.as_expression()
8064 && e.kind() == ExpressionKind::Invalid
8065 {
8066 if let Some(c) = e.invalid_child() {
8067 stack.push(c.as_sem_node());
8068 }
8069 continue;
8070 }
8071 if let Some(e) = n.as_expression() {
8072 if crate::symid::is_value_name(e.kind()) {
8073 if let Some(sym) = e.referenced_symbol()
8074 && seen.insert(sym.uid())
8075 {
8076 order.push(e);
8077 }
8078 continue;
8079 }
8080 if e.kind() == ExpressionKind::Assignment {
8081 // Skip only the LHS's own bare `NamedValue` node of a PLAIN
8082 // (`=`/`<=`) assignment (never a "read" for implicit-
8083 // sensitivity purposes, whether blocking or nonblocking) --
8084 // a compound lvalue (a bit-select, concatenation, ...) is
8085 // walked normally instead, so e.g. `arr[i] = x`'s index
8086 // expression `i` (and `arr`'s own identity) are still
8087 // collected, matching the "safe over-approximation" posture
8088 // above.
8089 //
8090 // A COMPOUND assignment (`+=`, `-=`, `&=`, ...) is never
8091 // desugared by slang -- `right()` holds only the RHS
8092 // operand (e.g. `a` for `q += a`), never `q + a` -- so a
8093 // bare `NamedValue` LHS genuinely is also a read there (per
8094 // IEEE 1800's implicit-sensitivity rule: a variable is
8095 // excluded from the inferred `@*` list only when it is
8096 // written but never read, which a compound assignment's LHS
8097 // is not). Detect this via `is_compound_assignment()` and
8098 // walk the LHS unconditionally in that case.
8099 let lhs_is_read = e.is_compound_assignment() == Some(true);
8100 if let Some(lhs) = e.left()
8101 && (lhs_is_read || !crate::symid::is_value_name(lhs.kind()))
8102 {
8103 stack.push(lhs.as_sem_node());
8104 }
8105 if let Some(rhs) = e.right() {
8106 stack.push(rhs.as_sem_node());
8107 }
8108 continue;
8109 }
8110 }
8111 stack.extend(n.children_with_invalid());
8112 }
8113 order
8114}
8115
8116/// LRM 6.14: a chandle "shall not be used ... in sensitivity lists or event
8117/// expressions"; slang does not diagnose it, so refuse it by name here
8118/// instead of waiting on a value that can never be observed to change.
8119fn reject_chandle_event(signal: &Expression<'_>) -> Result<(), LoweringError> {
8120 if signal.expr_type().is_some_and(|t| t.is_chandle()) {
8121 return Err(LoweringError::unsupported(
8122 "a chandle in an event expression (illegal, LRM 1800-2023 6.14)".to_string(),
8123 ));
8124 }
8125 Ok(())
8126}
8127
8128/// The operands `(memory, input_terms, output_terms)` of `stmt` when it is a
8129/// `$async$<and|or|nand|nor>$<array|plane>` call (LRM 20.16), else `None`.
8130pub fn async_pla_call<'d>(stmt: &Statement<'d>) -> Option<Vec<Expression<'d>>> {
8131 let e = stmt.expr()?;
8132 if e.kind() != ExpressionKind::Call || e.is_system_call() != Some(true) {
8133 return None;
8134 }
8135 let name = e.system_subroutine()?.name();
8136 let rest = name.strip_prefix("$async$")?;
8137 let (logic, format) = rest.split_once('$')?;
8138 (matches!(logic, "and" | "or" | "nand" | "nor") && matches!(format, "array" | "plane"))
8139 .then(|| e.operands())
8140}
8141
8142/// What an `$async$...` PLA call re-evaluates on: the personality memory and
8143/// everything the input terms read, never the output terms it writes.
8144fn async_pla_read_set<'d>(stmt: &Statement<'d>) -> Vec<Expression<'d>> {
8145 let ops = async_pla_call(stmt).unwrap_or_default();
8146 let root = |x: Expression<'d>| {
8147 let x = if x.kind() == ExpressionKind::Assignment {
8148 x.left().unwrap_or(x)
8149 } else {
8150 x
8151 };
8152 crate::symid::is_value_name(x.kind())
8153 .then(|| x.referenced_symbol())
8154 .flatten()
8155 .map(|s| (x, s.uid()))
8156 };
8157 let out_uid = ops.get(2).and_then(|o| root(*o)).map(|(_, u)| u);
8158 let mut reads: Vec<Expression<'d>> = implicit_comb_read_set(stmt)
8159 .into_iter()
8160 .filter(|r| r.referenced_symbol().map(|s| s.uid()) != out_uid)
8161 .collect();
8162 if let Some((mem, uid)) = ops.first().and_then(|o| root(*o))
8163 && !reads
8164 .iter()
8165 .any(|r| r.referenced_symbol().map(|s| s.uid()) == Some(uid.clone()))
8166 {
8167 reads.push(mem);
8168 }
8169 reads
8170}
8171
8172/// Task 3 (CG-170): extracts a `SignalEventControl`'s optional `iff`
8173/// condition. **Verified live** against the pinned `sv-lang = "=0.1.9"`
8174/// source (no `.iffCondition` accessor exists in this binding at all): per
8175/// slang's own `SignalEventControl::visitExprs` (`include/slang/ast/
8176/// TimingControl.h`: `expr.visit(visitor); if (iffCondition)
8177/// iffCondition->visit(visitor);`), which is what backs `children()`
8178/// (`CApiAst.cpp`'s generic `SemChildCollector`), a `SignalEvent`
8179/// `TimingControl` node's `children()` returns exactly 1 entry (the event
8180/// expression only) with no `iff`, or exactly 2 (event expression, then
8181/// `iff`, in that order) when one is present. Extraction is therefore
8182/// purely positional: `len() == 2`'s `[1]` is `iff`, `None` otherwise.
8183fn extract_iff<'d>(tc: &TimingControl<'d>) -> Option<Expression<'d>> {
8184 let children = tc.children();
8185 if children.len() == 2 {
8186 children[1].as_expression()
8187 } else {
8188 None
8189 }
8190}
8191
8192/// The pass and fail statements of an immediate assertion when one of them
8193/// waits (LRM 16.3: they are ordinary statements, so `#d`/`@e`/`wait` are
8194/// legal there), else `None` (the assertion stays a straight-line leaf). A
8195/// deferred assertion's action is a subroutine call that is queued (16.4), so
8196/// a waiting one is refused by name.
8197fn suspending_assertion_actions<'d>(
8198 stmt: &Statement<'d>,
8199) -> Option<(Option<Statement<'d>>, Option<Statement<'d>>)> {
8200 let pass = stmt.immediate_assertion_if_true();
8201 let fail = stmt.immediate_assertion_if_false();
8202 let waits = |s: &Option<Statement<'d>>| {
8203 s.as_ref()
8204 .is_some_and(|s| contains_suspend_trigger(s.as_sem_node()))
8205 };
8206 (waits(&pass) || waits(&fail)).then_some((pass, fail))
8207}
8208
8209/// The expression an immediate assertion checks.
8210pub fn assertion_cond<'d>(stmt: &Statement<'d>) -> Option<Expression<'d>> {
8211 stmt.as_sem_node()
8212 .children()
8213 .into_iter()
8214 .find_map(|n| n.as_expression())
8215}
8216
8217/// Whether `stmt` is an `expect` statement (LRM 1800-2023 §16.17): slang models
8218/// it as a concurrent assertion of kind `Expect`.
8219fn is_expect(stmt: &Statement<'_>) -> bool {
8220 stmt.kind() == StatementKind::ConcurrentAssertion
8221 && stmt.assertion_kind() == Some(sv_lang::AssertionKind::Expect)
8222}
8223
8224/// Generic depth-first scan (mirrors `vj-codegen::module::
8225/// reject_unsupported_in_body`'s pattern) for any statement kind
8226/// [`CfgBuilder::walk_stmt`] treats as suspend-triggering, wherever it sits
8227/// inside `node`'s subtree. Used only by the "ordinary straight-line" match
8228/// arm: a construct this task does not decompose (loops, `case`, ...) is
8229/// safe to keep as one opaque leaf `Statement` only as long as none of its
8230/// descendants secretly suspends.
8231fn contains_suspend_trigger(node: SemNode<'_>) -> bool {
8232 contains_suspend_trigger_in(node, &mut std::collections::HashSet::new(), true, true)
8233}
8234
8235/// `contains_suspend_trigger`, also looking through calls to user tasks
8236/// (whose bodies are inlined when they can suspend); `visiting` breaks
8237/// recursion through recursive tasks. `by_request` says whether a
8238/// `p.await();` / `p.suspend();` statement counts: not inside a function
8239/// body, which cannot be inlined as a suspending task (a `suspend()` there
8240/// takes effect at the caller's next blocking point instead).
8241///
8242/// `disable_is_trigger`: whether a `disable label;` counts (it is a jump the
8243/// CFG decomposes, but never itself suspends the process).
8244fn contains_suspend_trigger_in(
8245 node: SemNode<'_>,
8246 visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8247 by_request: bool,
8248 disable_is_trigger: bool,
8249) -> bool {
8250 let mut stack = vec![node];
8251 while let Some(n) = stack.pop() {
8252 if let Some(e) = n.as_expression()
8253 && let Some(callee) = user_task_callee(&e)
8254 && is_suspending_virtual(&callee)
8255 {
8256 return true;
8257 }
8258 if let Some(e) = n.as_expression()
8259 && e.kind() == ExpressionKind::Assignment
8260 && e.assignment_timing().is_some()
8261 && (e.is_nonblocking() != Some(true)
8262 || e.assignment_timing()
8263 .and_then(|t| t.as_timing_control())
8264 .is_some_and(|tc| tc.kind() != TimingControlKind::Delay))
8265 {
8266 // `lhs = #d rhs;` / `lhs = @(e) rhs;` / `q <= @(e) rhs;` (LRM 9.4.5,
8267 // 10.4.2): the assignment itself waits (`walk_intra_assignment_timing`,
8268 // `walk_nba_event`), so a task holding one suspends and is inlined --
8269 // otherwise `disable task;` of another process cannot reach it (9.6.2).
8270 return true;
8271 }
8272 if let Some(e) = n.as_expression() {
8273 // An interface `extern` call runs the module-side
8274 // implementations (LRM 25.7): their bodies are what can suspend.
8275 // A call to an `extern forkjoin` task nothing implements is a
8276 // run-time error the CFG reports (`Terminator::Switch`).
8277 let callees: Vec<Symbol<'_>> = match crate::netlist::itf::call_extern_targets(&e) {
8278 Some(t) if t.impls.is_empty() => return true,
8279 Some(t) => t.impls,
8280 None => user_task_callee(&e).into_iter().collect(),
8281 };
8282 for callee in callees {
8283 // A function with an `output`/`inout`/`ref` event formal is
8284 // inlined like a suspending one (`has_event_out_formal`).
8285 if has_event_out_formal(&callee) {
8286 return true;
8287 }
8288 if let Some(body) = proc_body(&callee)
8289 && visiting.insert(callee.uid())
8290 && contains_suspend_trigger_in(
8291 body.as_sem_node(),
8292 visiting,
8293 by_request
8294 && callee.subroutine_kind() == Some(sv_lang::SubroutineKind::Task),
8295 // A function is compiled, never inlined: its own
8296 // `disable blk;` is a branch there (codegen `func`).
8297 disable_is_trigger
8298 && callee.subroutine_kind() == Some(sv_lang::SubroutineKind::Task),
8299 )
8300 {
8301 return true;
8302 }
8303 }
8304 }
8305 if let Some(s) = n.as_statement() {
8306 let kind = s.kind();
8307 if by_request
8308 && kind == StatementKind::ExpressionStatement
8309 && is_process_by_request_stmt(&s)
8310 {
8311 return true;
8312 }
8313 let is_join_block = kind == StatementKind::Block
8314 && matches!(
8315 s.block_kind(),
8316 Some(
8317 StatementBlockKind::JoinAll
8318 | StatementBlockKind::JoinAny
8319 | StatementBlockKind::JoinNone
8320 )
8321 );
8322 if is_join_block
8323 || is_expect(&s)
8324 || matches!(
8325 kind,
8326 StatementKind::Timed
8327 | StatementKind::Wait
8328 | StatementKind::WaitFork
8329 | StatementKind::WaitOrder
8330 | StatementKind::EventTrigger
8331 | StatementKind::DisableFork
8332 )
8333 // `disable label;` is a jump in the CFG (see this file's
8334 // module doc comment) -- one hidden inside an opaque
8335 // "ordinary" leaf (e.g. a `case` branch) forces that leaf to
8336 // be decomposed too.
8337 || (disable_is_trigger && kind == StatementKind::Disable)
8338 {
8339 return true;
8340 }
8341 // A static or procedural concurrent assertion (16.14) is evaluated by the
8342 // assertion engine, never by the process that reaches it: a task its
8343 // property calls from a sequence match item (16.11) runs as a thread of
8344 // its own, so it suspends no process here. Only the pass/fail action
8345 // statements run in the hosting process and can hold a suspend point.
8346 if kind == StatementKind::ConcurrentAssertion {
8347 stack.extend(
8348 live_children(&n)
8349 .into_iter()
8350 .filter(|c| c.as_statement().is_some()),
8351 );
8352 continue;
8353 }
8354 }
8355 stack.extend(live_children(&n));
8356 }
8357 false
8358}
8359
8360/// Whether `task` can call itself ([`task_reaches`]), memoized per task in
8361/// `vt` when there is one.
8362fn task_reaches_itself<'a>(task: &Symbol<'a>, vt: Option<&VirtualTasks<'a>>) -> bool {
8363 let uid = task.uid();
8364 if let Some(vt) = vt
8365 && let Some(r) = vt.self_reach.borrow().get(&uid)
8366 {
8367 return *r;
8368 }
8369 let r = task_reaches(task, &uid, vt, &mut std::collections::HashSet::new());
8370 if let Some(vt) = vt {
8371 vt.self_reach.borrow_mut().insert(uid, r);
8372 }
8373 r
8374}
8375
8376/// Whether `sub`'s body calls `target` (directly or through the user tasks
8377/// and functions it calls). A call of a virtual method reaches every
8378/// implementation `vt` knows of (LRM 1800-2023 8.20), so recursion through
8379/// dynamic dispatch (`a.f()` -> override -> `a.f()` on a child) is found.
8380fn task_reaches<'a>(
8381 sub: &Symbol<'a>,
8382 target: &crate::symid::SymbolId,
8383 vt: Option<&VirtualTasks<'a>>,
8384 visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8385) -> bool {
8386 let Some(body) = proc_body(sub) else {
8387 return false;
8388 };
8389 let mut stack = vec![body.as_sem_node()];
8390 while let Some(n) = stack.pop() {
8391 if let Some(e) = n.as_expression()
8392 && let Some(callee) = user_task_callee(&e)
8393 {
8394 let mut callees = vec![callee];
8395 // `super.m()` is not virtual (LRM 8.15): it never reaches the
8396 // overrides, so it must not be seen as recursion through them.
8397 if let Some(vt) = vt
8398 && !is_explicit_super(e)
8399 && (callee
8400 .subroutine_flags()
8401 .contains(sv_lang::MethodFlags::VIRTUAL)
8402 || callee.subroutine_override().is_some())
8403 {
8404 callees.extend(vt.implementations(&callee).into_iter().map(|(_, s)| s));
8405 }
8406 for c in callees {
8407 if c.uid() == *target {
8408 return true;
8409 }
8410 if visiting.insert(c.uid()) && task_reaches(&c, target, vt, visiting) {
8411 return true;
8412 }
8413 }
8414 }
8415 stack.extend(live_children(&n));
8416 }
8417 false
8418}
8419
8420/// Whether `node`'s subtree calls a subroutine that is not a system task.
8421fn contains_subroutine_call(node: SemNode<'_>) -> bool {
8422 let mut stack = vec![node];
8423 while let Some(n) = stack.pop() {
8424 if let Some(e) = n.as_expression()
8425 && e.kind() == ExpressionKind::Call
8426 && e.is_system_call() != Some(true)
8427 {
8428 return true;
8429 }
8430 stack.extend(live_children(&n));
8431 }
8432 false
8433}
8434
8435/// The callee of a call to a user (non-system, non-DPI) task or function.
8436fn user_task_callee<'d>(e: &Expression<'d>) -> Option<Symbol<'d>> {
8437 if e.kind() != ExpressionKind::Call || e.is_system_call() == Some(true) {
8438 return None;
8439 }
8440 let callee = e.call_subroutine()?;
8441 if callee
8442 .subroutine_flags()
8443 .contains(sv_lang::MethodFlags::DPI_IMPORT)
8444 {
8445 return None;
8446 }
8447 Some(callee)
8448}
8449
8450/// The callee of `e` when it is a call to a user task whose body can
8451/// suspend -- such a call is inlined into the calling process's CFG.
8452fn suspending_task_callee<'d>(e: &Expression<'d>) -> Option<Symbol<'d>> {
8453 let callee = user_task_callee(e)?;
8454 subroutine_can_suspend(&callee).then_some(callee)
8455}
8456
8457/// The calls to user functions that can suspend (a `fork` in the body)
8458/// under `node`, innermost (argument) calls first.
8459fn collect_suspending_function_calls<'d>(node: SemNode<'d>, out: &mut Vec<Expression<'d>>) {
8460 for c in node.children() {
8461 collect_suspending_function_calls(c, out);
8462 }
8463 if let Some(e) = node.as_expression()
8464 && let Some(callee) = user_task_callee(&e)
8465 && callee.subroutine_kind() == Some(sv_lang::SubroutineKind::Function)
8466 && subroutine_can_suspend(&callee)
8467 {
8468 out.push(e);
8469 }
8470}
8471
8472/// Whether `sub` has an `output`/`inout`/`ref` `event` formal: its handle is
8473/// copied back to the actual (LRM 1800-2023 15.5.5.1), which only an inlined
8474/// body can do, so every call to it is inlined (value functions through
8475/// `hoist_suspending_function_calls`).
8476fn has_event_out_formal(sub: &Symbol<'_>) -> bool {
8477 sub.subroutine_arguments().any(|a| {
8478 a.formal_argument_direction() != sv_lang::ArgumentDirection::In
8479 && a.value_type().is_some_and(|t| t.is_event())
8480 })
8481}
8482
8483/// The key under which the code generator finds the capture temporary that
8484/// holds the value of the inlined call `call` ([`ArgBinding::CallResult`]).
8485pub fn call_result_key(call: &Expression<'_>) -> crate::symid::SymbolId {
8486 let at = call.source_range().start;
8487 crate::symid::SymbolId::synthetic_tagged(&format!("callret.{}.{}", at.buffer, at.offset))
8488}
8489
8490/// Whether `sub`'s body (or a task it calls) contains a suspend point --
8491/// every call to such a task is inlined into the calling process's CFG
8492/// instead of being compiled as a function.
8493pub fn subroutine_can_suspend(sub: &Symbol<'_>) -> bool {
8494 if is_suspending_virtual(sub) {
8495 return true;
8496 }
8497 let Some(body) = proc_body(sub) else {
8498 return false;
8499 };
8500 // An `output`/`inout`/`ref` event formal is copied back as a handle
8501 // (LRM 1800-2023 15.5.5.1), which only an inlined task can do.
8502 if has_event_out_formal(sub) {
8503 return true;
8504 }
8505 let mut visiting = std::collections::HashSet::new();
8506 visiting.insert(sub.uid());
8507 let by_request = sub.subroutine_kind() == Some(sv_lang::SubroutineKind::Task);
8508 contains_suspend_trigger_in(body.as_sem_node(), &mut visiting, by_request, by_request)
8509 || (sub.subroutine_kind() == Some(sv_lang::SubroutineKind::Task)
8510 && task_stages_nba(sub, &mut std::collections::HashSet::new()))
8511}
8512
8513/// Whether the task `sub` (or a task it calls) runs a nonblocking assignment
8514/// (LRM 1800-2023 10.4.2). Staging an NBA needs the calling process's
8515/// scheduler handle, which a compiled function does not have, so such a task
8516/// is inlined into each calling process like a suspending one -- it just
8517/// never reaches a suspend point.
8518fn task_stages_nba(
8519 sub: &Symbol<'_>,
8520 visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8521) -> bool {
8522 let Some(body) = proc_body(sub) else {
8523 return false;
8524 };
8525 let mut stack = vec![body.as_sem_node()];
8526 while let Some(n) = stack.pop() {
8527 if let Some(e) = n.as_expression() {
8528 if e.kind() == ExpressionKind::Assignment && e.is_nonblocking() == Some(true) {
8529 return true;
8530 }
8531 if let Some(callee) = user_task_callee(&e)
8532 && callee.subroutine_kind() == Some(sv_lang::SubroutineKind::Task)
8533 && visiting.insert(callee.uid())
8534 && task_stages_nba(&callee, visiting)
8535 {
8536 return true;
8537 }
8538 }
8539 stack.extend(live_children(&n));
8540 }
8541 false
8542}
8543
8544/// The method name when `callee` is a method of the built-in `std::process`
8545/// class (LRM 9.7): the class named `process` whose scope chain reaches the
8546/// `std` package -- scope identity, as `vj-codegen`'s
8547/// `is_builtin_std_class` does (a user class `process` is not it).
8548pub fn std_process_method(callee: &Symbol<'_>) -> Option<String> {
8549 std_class_method(callee, "process")
8550}
8551
8552/// The method name when `callee` is a method of the built-in `std` package
8553/// class named `class_name` (`process`, `semaphore`, `mailbox`): scope
8554/// identity, so a user class of the same name is not it.
8555pub fn std_class_method(callee: &Symbol<'_>, class_name: &str) -> Option<String> {
8556 let class = callee.parent()?;
8557 if !matches!(
8558 class.kind(),
8559 SymbolKind::ClassType | SymbolKind::GenericClassDef
8560 ) || class.name() != class_name
8561 {
8562 return None;
8563 }
8564 let mut scope = class.parent();
8565 while let Some(s) = scope {
8566 if s.kind() == SymbolKind::Package && s.name() == "std" {
8567 return Some(callee.name().to_string());
8568 }
8569 scope = s.parent();
8570 }
8571 None
8572}
8573
8574/// Whether event expression `e` names a CLASS event (LRM 1800-2023 15.5): an
8575/// instance `event` property (`m_event`, `obj.m_event`, `h[k].raised`), or an
8576/// `event` local or formal declared inside a class method. Such an event is a
8577/// runtime object (`sched::class_event`), not a row of a module's event pool.
8578/// Conversions are looked through; `null` is no class event by itself.
8579pub fn is_class_event_expr(e: &Expression<'_>) -> bool {
8580 let mut e = *e;
8581 while e.kind() == ExpressionKind::Conversion
8582 && let Some(inner) = e.conversion_operand()
8583 {
8584 e = inner;
8585 }
8586 if !e.expr_type().is_some_and(|t| t.is_event()) {
8587 return false;
8588 }
8589 let sym = if e.kind() == ExpressionKind::MemberAccess {
8590 e.member_symbol()
8591 } else if matches!(
8592 e.kind(),
8593 ExpressionKind::NamedValue | ExpressionKind::HierarchicalValue
8594 ) {
8595 e.referenced_symbol()
8596 } else {
8597 None
8598 };
8599 let Some(sym) = sym else {
8600 return false;
8601 };
8602 if sym.kind() == SymbolKind::ClassProperty {
8603 return true;
8604 }
8605 // A local/formal: the nearest enclosing subroutine is a class method.
8606 let mut scope = sym.parent();
8607 while let Some(s) = scope {
8608 if s.kind() == SymbolKind::Subroutine {
8609 return s.parent().is_some_and(|p| {
8610 matches!(
8611 p.kind(),
8612 SymbolKind::ClassType | SymbolKind::GenericClassDef
8613 )
8614 });
8615 }
8616 if matches!(
8617 s.kind(),
8618 SymbolKind::ClassType | SymbolKind::GenericClassDef
8619 ) {
8620 return false;
8621 }
8622 scope = s.parent();
8623 }
8624 false
8625}
8626
8627/// The class events `tc` waits on when it is `@e` on a class event or an
8628/// `@(e1 or e2 ...)` list with class events (no edge on a class term): waits
8629/// that queue through the runtime ([`is_class_event_expr`]). Empty when `tc`
8630/// waits on no class event. A list that also has signal or module-event terms
8631/// is [`class_event_wait_is_mixed`]; an edge on a class term is a named error,
8632/// and so is an `iff` on a signal term of a mixed list (its guard would also
8633/// gate the class wake). A class term's `iff` is judged after the wake
8634/// ([`class_event_term_iffs`]).
8635pub fn class_event_wait_terms<'d>(
8636 tc: &TimingControl<'d>,
8637) -> Result<Vec<Expression<'d>>, LoweringError> {
8638 let kind = tc.kind();
8639 if kind == TimingControlKind::SignalEvent {
8640 let Some(sig) = first_expression_child(tc) else {
8641 return Ok(Vec::new());
8642 };
8643 if !is_class_event_expr(&sig) {
8644 return Ok(Vec::new());
8645 }
8646 if tc.edge() != EdgeKind::None {
8647 return Err(LoweringError::unsupported(
8648 "an edge event control on a class `event` (LRM 1800-2023 15.5)",
8649 ));
8650 }
8651 return Ok(vec![sig]);
8652 }
8653 if kind == TimingControlKind::EventList {
8654 let mut terms = Vec::new();
8655 let mut plain_iff = false;
8656 for child in tc.children() {
8657 let Some(sub) = child.as_timing_control() else {
8658 continue;
8659 };
8660 match first_expression_child(&sub) {
8661 Some(sig)
8662 if sub.kind() == TimingControlKind::SignalEvent
8663 && is_class_event_expr(&sig) =>
8664 {
8665 if sub.edge() != EdgeKind::None {
8666 return Err(LoweringError::unsupported(
8667 "an edge term on a class `event` in an `@(a or b)` event list (LRM 1800-2023 15.5)",
8668 ));
8669 }
8670 terms.push(sig);
8671 }
8672 _ => {
8673 if extract_iff(&sub).is_some() {
8674 plain_iff = true;
8675 }
8676 }
8677 }
8678 }
8679 if plain_iff && !terms.is_empty() {
8680 return Err(LoweringError::unsupported(
8681 "an `iff` on a signal term of an `@(a or b)` event list that also has a class `event` term (the guard would gate the class wake too; LRM 1800-2023 9.4.2.3, 15.5)",
8682 ));
8683 }
8684 return Ok(terms);
8685 }
8686 Ok(Vec::new())
8687}
8688
8689/// The `iff` condition of each class-event term of the list `tc`, aligned
8690/// with [`class_event_wait_terms`] (the runtime queues a process on a term by
8691/// its index there). Empty unless `tc` is an event list.
8692pub fn class_event_term_iffs<'d>(tc: &TimingControl<'d>) -> Vec<Option<Expression<'d>>> {
8693 let mut out = Vec::new();
8694 if tc.kind() != TimingControlKind::EventList {
8695 return out;
8696 }
8697 for child in tc.children() {
8698 let Some(sub) = child.as_timing_control() else {
8699 continue;
8700 };
8701 if sub.kind() == TimingControlKind::SignalEvent
8702 && first_expression_child(&sub).is_some_and(|sig| is_class_event_expr(&sig))
8703 {
8704 out.push(extract_iff(&sub));
8705 }
8706 }
8707 out
8708}
8709
8710/// Whether `tc` is an `@(a or b)` event list that mixes class `event` terms
8711/// with signal or module-event terms (LRM 1800-2023 9.4.2.1, 15.5): such a
8712/// wait is both a class-event queue entry and a sensitivity-table wait.
8713pub fn class_event_wait_is_mixed(tc: &TimingControl<'_>) -> bool {
8714 if tc.kind() != TimingControlKind::EventList {
8715 return false;
8716 }
8717 let (mut class, mut plain) = (false, false);
8718 for child in tc.children() {
8719 let Some(sub) = child.as_timing_control() else {
8720 continue;
8721 };
8722 match first_expression_child(&sub) {
8723 Some(sig)
8724 if sub.kind() == TimingControlKind::SignalEvent && is_class_event_expr(&sig) =>
8725 {
8726 class = true;
8727 }
8728 _ => plain = true,
8729 }
8730 }
8731 class && plain
8732}
8733
8734/// Whether `stmt` is `p.await();`, `p.suspend();`, a blocking
8735/// `semaphore.get(...)` or a mailbox `put(...)`/`get(...)`/`peek(...)` as a
8736/// statement of its own (not nested in a larger expression): the `std` calls
8737/// that are a suspend point, because the caller may stop there.
8738fn is_process_by_request_stmt(stmt: &Statement<'_>) -> bool {
8739 let Some(e) = stmt.expr() else {
8740 return false;
8741 };
8742 if e.kind() != ExpressionKind::Call || e.is_system_call() == Some(true) {
8743 return false;
8744 }
8745 let Some(callee) = e.call_subroutine() else {
8746 return false;
8747 };
8748 std_process_method(&callee).is_some_and(|m| m == "await" || m == "suspend")
8749 || std_class_method(&callee, "semaphore").is_some_and(|m| m == "get")
8750 || std_class_method(&callee, "mailbox")
8751 .is_some_and(|m| matches!(m.as_str(), "put" | "get" | "peek"))
8752}
8753
8754/// Whether `stmt` is a mailbox `get(x);` / `peek(x);`: the blocking calls whose
8755/// result is delivered after the process resumes (the CFG emits the statement
8756/// again after its suspend point). A `put` finishes in its one call.
8757fn is_mailbox_two_phase_stmt(stmt: &Statement<'_>) -> bool {
8758 let Some(e) = stmt.expr() else {
8759 return false;
8760 };
8761 e.kind() == ExpressionKind::Call
8762 && e.is_system_call() != Some(true)
8763 && e.call_subroutine()
8764 .and_then(|callee| std_class_method(&callee, "mailbox"))
8765 .is_some_and(|m| m == "get" || m == "peek")
8766}
8767
8768/// Whether a call to `sub` can end the calling process from inside it: its
8769/// body, or a subroutine it calls, runs `$finish`/`$stop`/`$fatal` or a
8770/// task-form `$cast` that can fail, uses a
8771/// virtual interface (a `null` one is a fatal error, LRM 1800-2023 §25.9),
8772/// or calls a DPI import (which may call back into an export that does).
8773/// A call to a virtual method counts if ANY implementation in its override
8774/// chain can abort ([`mark_aborting_virtual`]: which override runs is only
8775/// known at run time), and a `new` expression counts if its constructor
8776/// chain or a property initializer can ([`class_construction_can_abort`]).
8777pub fn subroutine_can_abort(sub: &Symbol<'_>) -> bool {
8778 let mut visiting = std::collections::HashSet::new();
8779 can_abort_in(sub, &mut visiting)
8780}
8781
8782thread_local! {
8783 /// The uid of every virtual-method symbol (a base declaration, each
8784 /// override, an interface-class prototype) in a vtable slot at least one
8785 /// of whose implementations can abort. A call to a virtual method
8786 /// resolves statically to the BASE declaration, whose own body is empty,
8787 /// so [`can_abort_in`] cannot learn about an aborting override from the
8788 /// callee's body: codegen computes this set once, to a fixed point,
8789 /// before lowering any call site ([`mark_aborting_virtual`]).
8790 static ABORTING_VIRTUALS: std::cell::RefCell<HashSet<crate::symid::SymbolId>> =
8791 std::cell::RefCell::new(HashSet::new());
8792 /// Whether a `randomize()` call can end the process: some class's
8793 /// `pre_randomize()`/`post_randomize()` hook (which `randomize()` runs
8794 /// on the receiver or on any object it reaches, chosen by dynamic class)
8795 /// can abort ([`set_randomize_can_abort`]).
8796 static RANDOMIZE_CAN_ABORT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
8797}
8798
8799/// Empties the aborting-virtual set (and the `randomize()` flag); codegen
8800/// calls this before building a design's vtables.
8801pub fn reset_aborting_virtuals() {
8802 ABORTING_VIRTUALS.with(|s| s.borrow_mut().clear());
8803 RANDOMIZE_CAN_ABORT.with(|c| c.set(false));
8804}
8805
8806/// Declares that `randomize()` calls can abort (a `pre_randomize()` or
8807/// `post_randomize()` hook of the design can). Returns whether this changed the
8808/// flag, for a caller iterating to a fixed point.
8809pub fn set_randomize_can_abort() -> bool {
8810 RANDOMIZE_CAN_ABORT.with(|c| !c.replace(true))
8811}
8812
8813/// Whether [`set_randomize_can_abort`] has been called since the last reset.
8814pub fn randomize_can_abort() -> bool {
8815 RANDOMIZE_CAN_ABORT.with(|c| c.get())
8816}
8817
8818/// Records `uid` (a virtual method belonging to a slot with an aborting
8819/// implementation) as one whose calls can abort. Returns whether it was newly
8820/// recorded, so a caller iterating to a fixed point knows when it is done.
8821pub fn mark_aborting_virtual(uid: crate::symid::SymbolId) -> bool {
8822 ABORTING_VIRTUALS.with(|s| s.borrow_mut().insert(uid))
8823}
8824
8825fn can_abort_in(
8826 sub: &Symbol<'_>,
8827 visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8828) -> bool {
8829 if sub
8830 .subroutine_flags()
8831 .contains(sv_lang::MethodFlags::DPI_IMPORT)
8832 {
8833 return true;
8834 }
8835 if ABORTING_VIRTUALS.with(|s| s.borrow().contains(&sub.uid())) {
8836 return true;
8837 }
8838 if !visiting.insert(sub.uid()) {
8839 return false;
8840 }
8841 let Some(body) = proc_body(sub) else {
8842 return false;
8843 };
8844 if node_can_abort(body.as_sem_node(), Some(sub), visiting) {
8845 return true;
8846 }
8847 // A constructor's class-level work (property initializers, the
8848 // `extends Base(args)` call) runs with it, as part of the `new` that
8849 // invokes it.
8850 sub.subroutine_flags()
8851 .contains(sv_lang::MethodFlags::CONSTRUCTOR)
8852 && sub
8853 .parent()
8854 .and_then(|class| class.as_type())
8855 .is_some_and(|ty| class_level_can_abort(ty, visiting))
8856}
8857
8858/// Whether constructing an object of class `ty` (`new`: every constructor
8859/// from the root base class down, every level's property initializers and
8860/// `extends Base(args)` call) can end the process.
8861pub fn class_construction_can_abort(ty: sv_lang::Type<'_>) -> bool {
8862 let mut visiting = std::collections::HashSet::new();
8863 construction_can_abort(ty, &mut visiting)
8864}
8865
8866fn construction_can_abort(
8867 ty: sv_lang::Type<'_>,
8868 visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8869) -> bool {
8870 let mut level = Some(ty);
8871 while let Some(t) = level {
8872 if !t.is_class() {
8873 return false;
8874 }
8875 if class_level_can_abort(t, visiting) {
8876 return true;
8877 }
8878 if t.class_constructor()
8879 .is_some_and(|ctor| can_abort_in(&ctor, visiting))
8880 {
8881 return true;
8882 }
8883 level = t.class_base();
8884 }
8885 false
8886}
8887
8888/// Whether the work one class level does around its constructor body can
8889/// end the process: its property initializers and its `extends Base(args)`
8890/// call (the explicit-`super.new` form is part of the constructor body).
8891fn class_level_can_abort(
8892 ty: sv_lang::Type<'_>,
8893 visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8894) -> bool {
8895 let class_sym = ty.as_symbol();
8896 // `class A; A next = new; endclass` constructs itself: the guard keeps
8897 // the analysis finite (a class level is recorded once, under its own uid).
8898 if !visiting.insert(class_sym.uid()) {
8899 return false;
8900 }
8901 if let Some(call) = ty.class_base_constructor_call()
8902 && node_can_abort(call.as_sem_node(), None, visiting)
8903 {
8904 return true;
8905 }
8906 for member in class_sym.members() {
8907 if member.kind() == SymbolKind::ClassProperty
8908 && let Some(init) = member.initializer()
8909 && node_can_abort(init.as_sem_node(), None, visiting)
8910 {
8911 return true;
8912 }
8913 }
8914 false
8915}
8916
8917/// [`can_abort_in`]'s walk over one subtree. `enclosing` is the subroutine
8918/// the subtree belongs to (a `super.new(...)` finds its base class through
8919/// it).
8920fn node_can_abort<'d>(
8921 root: SemNode<'d>,
8922 enclosing: Option<&Symbol<'d>>,
8923 visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8924) -> bool {
8925 let mut stack = vec![root];
8926 while let Some(n) = stack.pop() {
8927 // `$cast(dest, src);` as a task is a run-time error when it fails
8928 // (LRM 6.24.2), ending the process like `$fatal`.
8929 if let Some(s) = n.as_statement()
8930 && s.kind() == StatementKind::ExpressionStatement
8931 && let Some(e) = s.expr()
8932 && is_failable_task_cast(&e)
8933 {
8934 return true;
8935 }
8936 if let Some(e) = n.as_expression() {
8937 if crate::netlist::vif::is_member_access(&e)
8938 || crate::netlist::vif::call_targets(&e).is_some()
8939 {
8940 return true;
8941 }
8942 if e.kind() == ExpressionKind::Call {
8943 if e.is_system_call() == Some(true) {
8944 if e.system_subroutine().is_some_and(|s| {
8945 match s.known_name_id() {
8946 KnownSystemName::Finish
8947 | KnownSystemName::Stop
8948 | KnownSystemName::Fatal => true,
8949 // `obj.randomize()` runs the class's
8950 // `pre_/post_randomize()` hooks (LRM 18.6.2).
8951 KnownSystemName::Randomize => randomize_can_abort(),
8952 _ => false,
8953 }
8954 }) {
8955 return true;
8956 }
8957 } else if let Some(callee) = e.call_subroutine() {
8958 // `p.kill()` ends the caller when `p` is the caller (or
8959 // one of its ancestors), which is only known at run time.
8960 if std_process_method(&callee).is_some_and(|m| m == "kill") {
8961 return true;
8962 }
8963 // An interface `extern` call runs its implementations.
8964 for target in crate::netlist::itf::call_callees(&e) {
8965 if can_abort_in(&target, visiting) {
8966 return true;
8967 }
8968 }
8969 }
8970 } else if e.kind() == ExpressionKind::NewClass {
8971 if let Some(ctor_call) = e.new_class_constructor_call() {
8972 stack.push(ctor_call.as_sem_node());
8973 }
8974 // `super.new(...)`'s expression type is `void`: the class
8975 // whose construction it continues is the enclosing
8976 // constructor's base. Any other `new` constructs its type.
8977 let constructed = if e.new_class_is_super_class() == Some(true) {
8978 enclosing
8979 .and_then(|sub| sub.parent())
8980 .and_then(|class| class.as_type())
8981 .and_then(|ty| ty.class_base())
8982 } else {
8983 e.expr_type().map(|t| t.canonical())
8984 };
8985 if constructed.is_some_and(|ty| construction_can_abort(ty, visiting)) {
8986 return true;
8987 }
8988 }
8989 }
8990 stack.extend(live_children(&n));
8991 }
8992 false
8993}
8994
8995/// Whether `e` is the task form of `$cast` whose destination type can reject
8996/// a value (a class handle or an enum; any other destination accepts every
8997/// source, so the call can never be the run-time error of LRM 6.24.2).
8998fn is_failable_task_cast(e: &Expression<'_>) -> bool {
8999 if e.kind() != ExpressionKind::Call
9000 || e.is_system_call() != Some(true)
9001 || !e
9002 .system_subroutine()
9003 .is_some_and(|s| s.known_name_id() == KnownSystemName::Cast)
9004 {
9005 return false;
9006 }
9007 let ops = e.operands();
9008 let Some(&dest) = ops.first() else {
9009 return false;
9010 };
9011 let dest = if dest.kind() == ExpressionKind::Assignment {
9012 dest.left().unwrap_or(dest)
9013 } else {
9014 dest
9015 };
9016 dest.expr_type()
9017 .is_some_and(|t| t.is_class() || t.is_enum())
9018}
9019
9020/// The target of an `output`/`inout` call argument: slang binds it as
9021/// `actual = <formal>`.
9022fn call_actual_lvalue<'d>(actual: Expression<'d>) -> Expression<'d> {
9023 if actual.kind() == ExpressionKind::Assignment {
9024 actual.left().unwrap_or(actual)
9025 } else {
9026 actual
9027 }
9028}
9029
9030/// The variables of the activation that forks `branch` that the branch
9031/// uses: automatic variables and inlined-task formals it does not declare
9032/// itself (static ones are the instance's signals).
9033fn captured_vars<'d>(branch: &ProcessCfg<'d>) -> Vec<Symbol<'d>> {
9034 let own: std::collections::HashSet<crate::symid::SymbolId> =
9035 super::liveness::declared_local_ids(branch);
9036 captured_in_excluding(vec![branch.body.as_sem_node()], &own)
9037}
9038
9039/// [`captured_vars`] over explicit AST roots, for a branch that declares nothing.
9040fn captured_in<'d>(roots: Vec<sv_lang::SemNode<'d>>) -> Vec<Symbol<'d>> {
9041 captured_in_excluding(roots, &std::collections::HashSet::new())
9042}
9043
9044fn captured_in_excluding<'d>(
9045 roots: Vec<sv_lang::SemNode<'d>>,
9046 own: &std::collections::HashSet<crate::symid::SymbolId>,
9047) -> Vec<Symbol<'d>> {
9048 let mut out: Vec<Symbol<'d>> = Vec::new();
9049 let mut seen = std::collections::HashSet::new();
9050 let mut stack = roots;
9051 while let Some(n) = stack.pop() {
9052 if let Some(e) = n.as_expression()
9053 && crate::symid::is_value_name(e.kind())
9054 && let Some(r) = e.referenced_symbol()
9055 {
9056 let procedural = (r.kind() == SymbolKind::Variable
9057 && r.variable_lifetime() == sv_lang::VariableLifetime::Automatic
9058 && r.parent().is_some_and(|p| {
9059 matches!(
9060 p.kind(),
9061 SymbolKind::StatementBlock | SymbolKind::Subroutine
9062 )
9063 }))
9064 || r.kind() == SymbolKind::FormalArgument;
9065 if procedural && !own.contains(&r.uid()) && seen.insert(r.uid()) {
9066 out.push(r);
9067 }
9068 }
9069 stack.extend(live_children(&n));
9070 }
9071 out
9072}
9073
9074/// The automatic variables the block statement `stmt` declares: created and
9075/// initialized each time the block is entered (LRM 1800-2023 §6.21), in
9076/// declaration order. A `for (int i = ...)` loop's variables live in the
9077/// implicit block around it, entered as the loop starts; with
9078/// `with_loop_vars` false they are left out (for a lowering that sets them
9079/// as part of the loop).
9080pub fn block_automatic_vars<'d>(stmt: &Statement<'d>, with_loop_vars: bool) -> Vec<Symbol<'d>> {
9081 if stmt.kind() != StatementKind::Block {
9082 return Vec::new();
9083 }
9084 let Some(block) = stmt.block_symbol() else {
9085 return Vec::new();
9086 };
9087 let mut loop_vars = std::collections::HashSet::new();
9088 if !with_loop_vars {
9089 let mut stack = stmt.statements();
9090 while let Some(s) = stack.pop() {
9091 if s.kind() == StatementKind::List {
9092 stack.extend(s.statements());
9093 } else if s.kind() == StatementKind::ForLoop {
9094 loop_vars.extend(s.for_loop_vars().iter().map(|v| v.uid()));
9095 }
9096 }
9097 }
9098 block
9099 .members()
9100 .filter(|m| {
9101 m.kind() == SymbolKind::Variable
9102 && m.variable_lifetime() == sv_lang::VariableLifetime::Automatic
9103 && !loop_vars.contains(&m.uid())
9104 })
9105 .collect()
9106}
9107
9108/// The prefix of the names vj gives the blocks it introduces into a
9109/// design (`randsequence`'s expansion): they are not part of any
9110/// hierarchical name the design prints.
9111pub const INTERNAL_NAME_PREFIX: &str = "__vj_";
9112
9113/// `sym`'s hierarchical name as the design sees it (`%m`, a message's
9114/// scope): the blocks vj introduced ([`INTERNAL_NAME_PREFIX`]) left out.
9115pub fn shown_path(sym: &Symbol<'_>) -> String {
9116 let path = sym.hierarchical_path();
9117 if !path.contains(INTERNAL_NAME_PREFIX) {
9118 return path;
9119 }
9120 let mut out = String::with_capacity(path.len());
9121 let mut rest = path.as_str();
9122 while let Some(i) = rest.find(INTERNAL_NAME_PREFIX) {
9123 let at_segment = i == 0 || rest.as_bytes()[i - 1] == b'.';
9124 if !at_segment {
9125 out.push_str(&rest[..i + INTERNAL_NAME_PREFIX.len()]);
9126 rest = &rest[i + INTERNAL_NAME_PREFIX.len()..];
9127 continue;
9128 }
9129 // Drop the segment and the `.` before it (or after it, at the start).
9130 let end = rest[i..].find('.').map_or(rest.len(), |e| i + e);
9131 if i > 0 {
9132 out.push_str(&rest[..i - 1]);
9133 rest = &rest[end..];
9134 } else {
9135 rest = rest[end..].strip_prefix('.').unwrap_or(&rest[end..]);
9136 }
9137 }
9138 out.push_str(rest);
9139 out
9140}
9141
9142/// Whether a variable of type `ty` can hold a class handle or a queue /
9143/// dynamic / associative array handle (directly or in a struct field): the
9144/// variables whose block exit owes a release. A fixed-size array of handles
9145/// is not covered (its elements are released on overwrite only).
9146pub fn may_hold_handles(ty: &sv_lang::Type<'_>) -> bool {
9147 if ty.is_class() || crate::state::storage::is_collection(ty) {
9148 return true;
9149 }
9150 ty.is_unpacked_struct()
9151 && ty
9152 .fields()
9153 .any(|f| f.value_type().is_some_and(|t| may_hold_handles(&t)))
9154}
9155
9156/// Whether `var` is an automatic variable a block declares (initialized on
9157/// entry to that block, [`block_automatic_vars`]).
9158pub fn is_automatic_block_var(var: &Symbol<'_>) -> bool {
9159 var.variable_lifetime() == sv_lang::VariableLifetime::Automatic
9160 && var
9161 .parent()
9162 .is_some_and(|p| p.kind() == SymbolKind::StatementBlock)
9163}
9164
9165/// A task's own variables: members of the task scope (not its formals or
9166/// result variable) and of every block inside its body.
9167fn task_locals<'d>(task: &Symbol<'d>) -> Vec<Symbol<'d>> {
9168 let formal_ids: std::collections::HashSet<crate::symid::SymbolId> =
9169 task.subroutine_arguments().map(|a| a.uid()).collect();
9170 let ret_id = task.subroutine_return_val_var().map(|r| r.uid());
9171 let mut out = Vec::new();
9172 let mut seen = std::collections::HashSet::new();
9173 let mut take = |m: Symbol<'d>, out: &mut Vec<Symbol<'d>>| {
9174 let id = m.uid();
9175 if m.kind() == SymbolKind::Variable
9176 && !formal_ids.contains(&id)
9177 && Some(&id) != ret_id.as_ref()
9178 && seen.insert(id)
9179 {
9180 out.push(m);
9181 }
9182 };
9183 for m in task.members() {
9184 take(m, &mut out);
9185 }
9186 if let Some(body) = proc_body(task) {
9187 let mut stack = vec![body.as_sem_node()];
9188 while let Some(n) = stack.pop() {
9189 if let Some(s) = n.as_statement() {
9190 for block in block_scopes(&s) {
9191 for m in block.members() {
9192 take(m, &mut out);
9193 }
9194 }
9195 }
9196 let mut kids = live_children(&n);
9197 kids.reverse();
9198 stack.extend(kids);
9199 }
9200 }
9201 out
9202}
9203
9204// --- Segment IR (PR-041, PR-042, PR-043; SegId numbering CG-098, CG-099) ---
9205
9206/// Task S3b's real liveness/frame-layout shape now lives in
9207/// `process::liveness` (`FrameLayout`/`FrameSlotDesc`/`FrameSlotSource`) --
9208/// re-exported here under this task's original name so every existing call
9209/// site (this file's own `lower_to_segments`, `tests/process_cfg.rs`) keeps
9210/// working unchanged.
9211pub use super::liveness::FrameLayout;
9212use super::liveness::FrameSlotSource;
9213
9214/// The codegen-facing, already-liveness-annotated lowering shape (PR-042).
9215/// `live_in`/`live_out` are `FrameLayout::slots` indices (Task S3b: see
9216/// `lower_to_segments`'s doc comment for exactly which slots those are).
9217#[derive(Debug, Clone)]
9218pub struct Segment<'d> {
9219 pub id: SegId,
9220 pub live_in: Vec<u32>,
9221 /// Mirrors [`BasicBlock::bindings`] 1:1 -- run before `body`.
9222 pub bindings: Vec<ArgBinding<'d>>,
9223 /// Mirrors [`BasicBlock::this_var`] 1:1.
9224 pub this_var: Option<Symbol<'d>>,
9225 /// Mirrors [`BasicBlock::ref_aliases`] 1:1.
9226 pub ref_aliases: Vec<(Symbol<'d>, Symbol<'d>)>,
9227 pub body: Vec<Statement<'d>>,
9228 /// Mirrors [`BasicBlock::probes`] 1:1 -- indices are into `body`.
9229 pub probes: Vec<(usize, Statement<'d>)>,
9230 /// Mirrors [`BasicBlock::disable_probes`] 1:1 -- indices are into `body`.
9231 pub disable_probes: Vec<(usize, Statement<'d>)>,
9232 /// Mirrors [`BasicBlock::frame_probes`] 1:1 -- indices are into `body`.
9233 pub frame_probes: Vec<(usize, FrameProbe<'d>)>,
9234 /// Mirrors [`BasicBlock::block_events`] 1:1 -- indices are into `body`.
9235 pub block_events: Vec<(usize, Symbol<'d>, bool)>,
9236 /// Mirrors [`BasicBlock::stmt_scopes`] 1:1 -- indices are into `body`.
9237 pub stmt_scopes: Vec<(usize, Symbol<'d>)>,
9238 /// Mirrors [`BasicBlock::step_exprs`] 1:1 -- see its doc comment.
9239 pub step_exprs: Vec<Expression<'d>>,
9240 /// Mirrors [`BasicBlock::repeat_init`] 1:1 -- see its doc comment.
9241 pub repeat_init: Option<(RepeatCounterId, RepeatCount<'d>)>,
9242 /// Mirrors [`BasicBlock::repeat_decrement`] 1:1 -- see its doc comment.
9243 pub repeat_decrement: Option<RepeatCounterId>,
9244 /// Mirrors [`BasicBlock::expect_resume`] 1:1 -- see its doc comment.
9245 pub expect_resume: bool,
9246 pub terminator: SegTerminator<'d>,
9247}
9248
9249/// PR-043: covers every [`Terminator`] variant with no lossy fallback (see
9250/// [`lower_to_segments`]'s 1:1 mapping).
9251#[derive(Debug, Clone)]
9252pub enum SegTerminator<'d> {
9253 Goto(SegId),
9254 CondGoto {
9255 cond: Expression<'d>,
9256 t: SegId,
9257 f: SegId,
9258 },
9259 Suspend {
9260 point: SuspendPoint<'d>,
9261 resume: SegId,
9262 live_out: Vec<u32>,
9263 },
9264 End {
9265 loop_back: Option<SegId>,
9266 },
9267 /// The lowering of [`Terminator::Switch`].
9268 SwitchGoto {
9269 on: SwitchOn<'d>,
9270 arms: Vec<SegId>,
9271 default: SegId,
9272 report: Option<sv_lang::SourceLoc>,
9273 },
9274 /// R-run Task 8: the lossless lowering of [`Terminator::RepeatCheck`] --
9275 /// see that variant's own doc comment for why a `repeat(N)` loop's
9276 /// per-iteration decision cannot be represented as an ordinary
9277 /// `CondGoto` (no `cond: Expression` exists for "compiler-synthesized
9278 /// counter != 0"). **Deviation from this file's own module doc comment**
9279 /// (Task 1's "frozen, codegen-exhausted `SegTerminator`" policy): that
9280 /// policy was written to rule out `ForkJoin` specifically (which has no
9281 /// honest `SegTerminator` shape at all, ever, this wave); a suspend
9282 /// nested inside a `repeat` body is a genuinely new kind of branch
9283 /// condition this crate had no way to construct before this task, not a
9284 /// reachability gap in the pre-existing four variants -- recorded in
9285 /// this task's own deviations.
9286 CondGotoCounter {
9287 counter: RepeatCounterId,
9288 t: SegId,
9289 f: SegId,
9290 },
9291 /// R-run Task 17 (PR-047/PR-061): the lossless lowering of
9292 /// [`Terminator::ForkJoin`] -- a real, new `SegTerminator` shape, added
9293 /// for the identical reason `CondGotoCounter` above already documents a
9294 /// deviation from this file's own "frozen `SegTerminator`" policy note:
9295 /// that note was written to rule out `ForkJoin` specifically because
9296 /// NOTHING had implemented fork/join scheduling yet; this task is that
9297 /// implementation for `StatementBlockKind::JoinAll` (codegen enforces
9298 /// the JoinAll-only scope with a named `LoweringError` for `JoinAny`/
9299 /// `JoinNone` -- this type carries `kind` through losslessly so codegen
9300 /// has it to check, rather than this crate silently narrowing what it
9301 /// represents). `branches` is exactly [`Terminator::ForkJoin`]'s own
9302 /// field, carried through unchanged (each branch is still a full,
9303 /// independently suspend-capable nested [`ProcessCfg`] -- codegen
9304 /// recursively lowers each one into its own real compiled kernel).
9305 /// `join_blk`/`live_out` mirror `Suspend`'s own `resume`/`live_out`
9306 /// fields exactly: a fork/join site suspends the calling process (until
9307 /// every spawned child completes, for `JoinAll`) just as surely as a
9308 /// `Suspend` terminator does, so the same permanent-frame-slot
9309 /// liveness/writeback discipline applies -- see [`lower_to_segments`]'s
9310 /// own fill rule.
9311 ForkJoin {
9312 kind: StatementBlockKind,
9313 branches: Vec<ProcessCfg<'d>>,
9314 join_blk: SegId,
9315 live_out: Vec<u32>,
9316 },
9317 /// The lowering of [`Terminator::Assertion`].
9318 AssertGoto {
9319 stmt: Statement<'d>,
9320 pass: SegId,
9321 fail: SegId,
9322 done: SegId,
9323 },
9324 /// The lowering of [`Terminator::Case`]: goes to `arms[i]` for the first
9325 /// item of `stmt` whose labels match its selector, else `default`.
9326 CaseGoto {
9327 stmt: Statement<'d>,
9328 arms: Vec<SegId>,
9329 default: SegId,
9330 },
9331}
9332
9333/// PR-041/042/043: a 1:1, source-order-stable (CG-098, CG-099) lowering of
9334/// each `ProcessCfg` [`BasicBlock`] into a [`Segment`] -- `blocks[i]` always
9335/// becomes `SegId(i)`, so segment numbering is fixed the moment `build_cfg`
9336/// allocates its blocks, well before this function (or any LLVM `Context`)
9337/// ever runs.
9338///
9339/// **Task S3b's `live_in`/`live_out` fill rule:** every `FrameLayout` slot
9340/// with source `FrameSlotSource::DeclaredVar` is a *permanent* slot
9341/// (PR-017/018 -- valid for the process's entire lifetime, never
9342/// added/removed by liveness), so its index belongs in `live_in`/`live_out`
9343/// at every segment, unconditionally. A `FrameSlotSource::Temporary` slot's
9344/// index would additionally need to appear only where
9345/// `liveness::compute_liveness`'s own backward fixpoint says it is actually
9346/// live; this task's own restricted CFG-level IR never has a positive source
9347/// for a `Temporary` slot at all (one is only ever minted once CODEGEN's
9348/// Task S3c starts decomposing expressions into named SSA values with no
9349/// corresponding `sv_lang` symbol), so `frame.slots` here is, for every
9350/// fixture this task can construct, exactly the `DeclaredVar` set -- see
9351/// `liveness.rs`'s own module doc comment for the full reasoning. A future
9352/// Task S3c revision of this function additionally intersects
9353/// `compute_liveness`'s per-segment result with whatever `Temporary` slots
9354/// exist by then; this rule does not change for `DeclaredVar` slots.
9355pub fn lower_to_segments<'d>(cfg: &ProcessCfg<'d>, frame: &FrameLayout) -> Vec<Segment<'d>> {
9356 // R-run Task 8: a `repeat` loop's synthetic counter is ALSO permanent --
9357 // it must survive across a suspend/resume call boundary exactly like a
9358 // declared local (see `FrameSlotSource::SyntheticCounter`'s own doc
9359 // comment), so its slot index belongs in `live_in`/`live_out` at every
9360 // segment unconditionally too, same rule as `DeclaredVar`.
9361 let permanent: Vec<u32> = frame
9362 .slots
9363 .iter()
9364 .enumerate()
9365 .filter(|(_, s)| {
9366 matches!(
9367 s.source,
9368 FrameSlotSource::DeclaredVar(_)
9369 | FrameSlotSource::SyntheticCounter(_)
9370 | FrameSlotSource::Temporary(_)
9371 )
9372 })
9373 .map(|(i, _)| i as u32)
9374 .collect();
9375
9376 cfg.blocks
9377 .iter()
9378 .enumerate()
9379 .map(|(i, b)| Segment {
9380 id: SegId(i as u32),
9381 live_in: permanent.clone(),
9382 bindings: b.bindings.clone(),
9383 this_var: b.this_var,
9384 ref_aliases: b.ref_aliases.clone(),
9385 body: b.stmts.clone(),
9386 probes: b.probes.clone(),
9387 disable_probes: b.disable_probes.clone(),
9388 frame_probes: b.frame_probes.clone(),
9389 block_events: b.block_events.clone(),
9390 stmt_scopes: b.stmt_scopes.clone(),
9391 step_exprs: b.step_exprs.clone(),
9392 repeat_init: b.repeat_init.clone(),
9393 repeat_decrement: b.repeat_decrement,
9394 expect_resume: b.expect_resume,
9395 terminator: lower_terminator(&b.terminator, &permanent),
9396 })
9397 .collect()
9398}
9399
9400/// Task 1: `true` iff any block in `cfg` closes with `Terminator::ForkJoin`
9401/// -- fork/join has no runtime scheduling semantics this wave (`docs/
9402/// design/process.md` §7: "construction only, never scheduled"), so
9403/// [`lower_to_segments`]/`lower_terminator` cannot honestly produce a
9404/// `SegTerminator` for one (there is no shape in that frozen, codegen-
9405/// exhausted enum for "spawn N branches" -- see this file's module doc
9406/// comment's `SegTerminator`-freeze note). Every real caller (this crate's
9407/// own `vj::run`) must check this and fail loudly *before* calling
9408/// `lower_to_segments` on a CFG that might contain one; `lower_terminator`
9409/// itself still documents and asserts the precondition (`unreachable!`) as
9410/// a caller-discipline internal invariant, exactly like `finish()`'s own
9411/// "every block must be closed" `.expect()` -- not a condition reachable
9412/// from malformed SV source once a caller has checked this predicate first.
9413pub fn contains_unschedulable_fork_join(cfg: &ProcessCfg<'_>) -> bool {
9414 cfg.blocks
9415 .iter()
9416 .any(|b| matches!(b.terminator, Terminator::ForkJoin { .. }))
9417}
9418
9419/// R-run Task 17 (PR-047/PR-061): `true` iff any block in `cfg` closes with
9420/// a `Terminator::ForkJoin` whose `kind` is `JoinAny`/`JoinNone` -- the two
9421/// fork/join shapes this task's own scope note ("Scope for this task:
9422/// `JoinAll` only") leaves unimplemented at the codegen layer.
9423/// **Deliberately narrower than [`contains_unschedulable_fork_join`]
9424/// above, which this function does NOT replace or modify** (that
9425/// function's existing callers -- `vj::aot`, which this task does
9426/// not extend to support ANY fork/join shape -- keep their original,
9427/// unchanged "reject every fork/join" behavior unaffected): a `JoinAll`
9428/// fork/join now has a real [`SegTerminator::ForkJoin`] shape
9429/// [`lower_to_segments`] can produce, so a caller that DOES support it
9430/// (`vj::run`) needs a predicate that only rejects the two kinds
9431/// still genuinely unsupported, letting `JoinAll` flow through to
9432/// `lower_to_segments`/codegen instead of being rejected pre-emptively.
9433pub fn contains_unsupported_fork_join_kind(cfg: &ProcessCfg<'_>) -> bool {
9434 cfg.blocks.iter().any(|b| {
9435 matches!(
9436 b.terminator,
9437 Terminator::ForkJoin {
9438 kind: StatementBlockKind::JoinAny | StatementBlockKind::JoinNone,
9439 ..
9440 }
9441 )
9442 })
9443}
9444
9445fn lower_terminator<'d>(t: &Terminator<'d>, permanent: &[u32]) -> SegTerminator<'d> {
9446 match t {
9447 Terminator::Fallthrough(b) => SegTerminator::Goto(SegId(b.0)),
9448 Terminator::Switch {
9449 on,
9450 arms,
9451 default_blk,
9452 report,
9453 } => SegTerminator::SwitchGoto {
9454 on: on.clone(),
9455 arms: arms.iter().map(|b| SegId(b.0)).collect(),
9456 default: SegId(default_blk.0),
9457 report: *report,
9458 },
9459 Terminator::Branch {
9460 cond,
9461 then_blk,
9462 else_blk,
9463 } => SegTerminator::CondGoto {
9464 cond: *cond,
9465 t: SegId(then_blk.0),
9466 f: SegId(else_blk.0),
9467 },
9468 Terminator::Suspend { point, resume_blk } => SegTerminator::Suspend {
9469 point: point.clone(),
9470 resume: SegId(resume_blk.0),
9471 live_out: permanent.to_vec(),
9472 },
9473 Terminator::End { loop_back } => SegTerminator::End {
9474 loop_back: loop_back.map(|b| SegId(b.0)),
9475 },
9476 // Task 1: `Loop` lowers losslessly to `SegTerminator::Goto` -- see
9477 // the module doc comment's `SegTerminator`-freeze note. Its real
9478 // per-iteration decision already lives on whichever block `entry`
9479 // names (a real `Branch`, or `Fallthrough` for `forever`), so
9480 // nothing this enum could represent is lost.
9481 Terminator::Loop { entry, .. } => SegTerminator::Goto(SegId(entry.0)),
9482 // R-run Task 8: see `SegTerminator::CondGotoCounter`'s own doc
9483 // comment for why this is a real, new lowering (not a lossless
9484 // `Goto`/`CondGoto` reuse the way `Loop` above is).
9485 Terminator::RepeatCheck {
9486 counter,
9487 then_blk,
9488 else_blk,
9489 } => SegTerminator::CondGotoCounter {
9490 counter: *counter,
9491 t: SegId(then_blk.0),
9492 f: SegId(else_blk.0),
9493 },
9494 // R-run Task 17: no longer unreachable -- see `SegTerminator::
9495 // ForkJoin`'s own doc comment. Lossless: `branches`/`kind` carry
9496 // straight through, `join_blk`/`live_out` get the exact same
9497 // treatment `Suspend`'s `resume`/`live_out` get above (a fork/join
9498 // site is a real suspend point too).
9499 Terminator::ForkJoin {
9500 kind,
9501 branches,
9502 join_blk,
9503 } => SegTerminator::ForkJoin {
9504 kind: *kind,
9505 branches: branches.clone(),
9506 join_blk: SegId(join_blk.0),
9507 live_out: permanent.to_vec(),
9508 },
9509 Terminator::Assertion {
9510 stmt,
9511 pass_blk,
9512 fail_blk,
9513 done_blk,
9514 } => SegTerminator::AssertGoto {
9515 stmt: *stmt,
9516 pass: SegId(pass_blk.0),
9517 fail: SegId(fail_blk.0),
9518 done: SegId(done_blk.0),
9519 },
9520 Terminator::Case {
9521 stmt,
9522 arms,
9523 default_blk,
9524 } => SegTerminator::CaseGoto {
9525 stmt: *stmt,
9526 arms: arms.iter().map(|b| SegId(b.0)).collect(),
9527 default: SegId(default_blk.0),
9528 },
9529 }
9530}
9531
9532// --- PR-074: cross-check against AnalyzedProcedure::timing_controls() ---
9533
9534/// Counts every `Timed` statement anywhere in `body`'s subtree, independent
9535/// of whether `build_cfg` would actually accept each one (a rejected
9536/// `TimingControlKind` is still a `Timed` statement) -- this subsystem's own
9537/// "how many suspend points did the tree walk find" half of PR-074's
9538/// cross-check.
9539pub fn count_timed_statements(body: &Statement<'_>) -> usize {
9540 let mut count = 0usize;
9541 let mut stack = vec![body.as_sem_node()];
9542 while let Some(n) = stack.pop() {
9543 if let Some(s) = n.as_statement()
9544 && s.kind() == StatementKind::Timed
9545 {
9546 count += 1;
9547 }
9548 stack.extend(live_children(&n));
9549 }
9550 count
9551}
9552
9553/// PR-074: "the subsystem's own suspend-point tree walk must be cross-checked
9554/// against `AnalyzedProcedure::timing_controls()`; a mismatch ... MUST be a
9555/// hard-fail bug, not a warning." Factored as a pure comparator over two
9556/// counts (rather than requiring every caller to thread a live
9557/// `sv_lang::Analysis` through) so a test can fault-inject a mismatch
9558/// directly -- see `cross_check_hard_fails_on_a_fault_injected_mismatch` in
9559/// `tests/process_cfg.rs`. The real call site passes
9560/// `own_walk_count = count_timed_statements(body)` and
9561/// `analyzed_count = analyzed_procedure.timing_controls().count()`.
9562pub fn check_suspend_count_matches(
9563 own_walk_count: usize,
9564 analyzed_count: usize,
9565) -> Result<(), LoweringError> {
9566 if own_walk_count == analyzed_count {
9567 Ok(())
9568 } else {
9569 Err(LoweringError::malformed(format!(
9570 "PR-074 suspend-point cross-check failed: this subsystem's own tree walk found \
9571 {own_walk_count} Timed statement(s), but AnalyzedProcedure::timing_controls() \
9572 found {analyzed_count} -- this is a hard bug (a missed or double-counted \
9573 suspend point), never a warning"
9574 )))
9575 }
9576}