Skip to main content

sv_sim_sim/process/
cfg.rs

1//! Task S3a (`docs/superpowers/plans/2026-09-26-wave-A2-slice.md`): the CPS
2//! control-flow-graph builder and its Segment IR lowering, restricted to
3//! exactly the A2-slice's two suspend kinds (`Delay`, `Edge` -- i.e.
4//! `SignalEventControl` with `posedge`/`negedge`). Ledger PR-004, PR-005,
5//! PR-006 (Delay/SignalEvent half only), PR-007, PR-008, PR-041, PR-042,
6//! PR-043, PR-044, PR-048, PR-069, PR-074, PR-114, CG-098, CG-099. See
7//! `docs/design/process.md` §§1-6.
8//!
9//! **Task 1 (`docs/superpowers/plans/2026-09-26-wave-A2-process-kernels.md`,
10//! ledger PR-006/PR-045) extends the above** with the rest of the
11//! suspend-relevant `StatementKind` taxonomy: `Wait` (level-sensitive,
12//! `SuspendPoint::Level` -- CFG shape only, the "sometimes there's no
13//! suspend at all" re-check behavior is Task 3's), `EventTrigger` (split
14//! only when it carries a `DelayControl` timing; an undelayed `->`/`->>`
15//! never suspends), a fork/join `Block` (`Terminator::ForkJoin`,
16//! construction only -- never scheduled, see `docs/design/process.md` §7),
17//! and `Terminator::Loop{header,body,exit,entry}` for `for`/`while`/
18//! `do-while`/`forever`/`foreach` (PR-045). `WaitFork`/`WaitOrder`/
19//! `DisableFork` remain named [`LoweringError`]s (recognized, never silently
20//! ignored, but not yet schedulable -- fork/join has no runtime semantics
21//! this wave either way). Two more `TimingControlKind`s get real handling:
22//! `EventList` (as `SuspendPoint::EdgeList`) and `CycleDelay` (lowered like
23//! `Delay` against a single inferred clock, matching `docs/design/
24//! process.md` §2's own explicitly-flagged stub scope).
25//! `ImplicitEvent`/`RepeatedEvent`/`Delay3`/`OneStepDelay`/`BlockEventList`
26//! remain named `LoweringError`s: `docs/design/process.md` §2's own
27//! `SuspendPoint` enum (the binding design record) has exactly six variants
28//! (`Delay`/`Edge`/`EdgeList`/`ImplicitComb`/`Level`/`CycleDelay`) and does
29//! not admit these five as first-class suspend shapes at all, and this
30//! file's own extension policy (below) forbids adding a placeholder variant
31//! ahead of a real implementation -- so PR-006 stays `"partial"` in
32//! `docs/ledger.json` after this task, not `"done"` (see this task's own
33//! deviations note for the full accounting).
34//!
35//! **Task 6 (`docs/superpowers/plans/2026-09-27-run-wiring.md`, ledger
36//! CG-081/CG-171) closes most of the above gap.** `SuspendPoint` gains real
37//! variants for `ImplicitEvent` (`ImplicitComb`) and `OneStepDelay`
38//! (`OneStep`), and `CycleDelay` is revised to carry its inferred clock
39//! signal/edge as real payload (see that variant's own doc comment for the
40//! full CG-171 reasoning, including a recorded deviation from the plan's
41//! literal `analysis::ClockDomainId`-carrying wire design). Two kinds stay
42//! named `LoweringError`s after this task too, each for a DIFFERENT,
43//! verified reason -- neither is "no `SuspendPoint` variant exists for it"
44//! any more: `BlockEventList` is only a covergroup sampling event (LRM 19.3;
45//! slang rejects it as a procedural timing control at parse, see the
46//! `BlockEventList` arm in `walk_timed`); `RepeatedEvent` is structurally UNREACHABLE at this dispatch
47//! point at all -- verified empirically (a throwaway AST-dumping test) that
48//! `RepeatedEventControl` is never the outer `TimingControl` of a
49//! `StatementKind::Timed` node, only ever an intra-assignment timing control
50//! embedded in an `Assignment` expression's own children, a structurally
51//! different position needing a `Temporary`-frame-slot CFG-splitting
52//! mechanism this AST-only layer does not have -- see the `RepeatedEvent`
53//! arm in `walk_timed` for the full citation and this task's deviations for
54//! the full accounting. `Delay3` remains rejected unconditionally too, but
55//! because it is semantically inapplicable to a procedural suspend point at
56//! all (gate-primitive delay syntax), not a gap.
57//!
58//! **Task 3 (`docs/superpowers/plans/2026-09-26-wave-A2-process-kernels.md`
59//! "A2-part2", ledger CG-169/CG-170/PR-106/CG-115) gives `Wait`/`SignalEvent`
60//! their real, previously-deferred semantics.** [`Self::walk_wait`] no
61//! longer closes `cur` with an unconditional `Terminator::Suspend` -- see its
62//! own doc comment for the `Terminator::Branch`-based "immediate check,
63//! conditional suspend" shape LRM 9.4.3/CG-169 requires. `SuspendPoint::Edge`
64//! gains an `iff: Option<Expression<'d>>` field (CG-170,
65//! `SignalEventControl.iffCondition`), extracted positionally from
66//! `TimingControl::children()` by [`extract_iff`] -- see that function's own
67//! doc comment for the verified-live `sv-lang`=0.1.9 binding shape this
68//! relies on. Neither change touches `SuspendPoint::EdgeList`: Task 1's own
69//! doc comment on that variant already scopes `iff` extraction to the
70//! narrower `(Expression, EdgeKind)` pair with no `iff`, and this task does
71//! not revisit that decision.
72//!
73//! **`disable label;` (`StatementKind::Disable`, distinct from
74//! `DisableFork`).** The pinned `sv-lang`=0.1.9 binding exposes no way to
75//! read `DisableStatement::target`: the C API has no `slang_stmt_disable_*`
76//! accessor, and the generic `children()`/`expressions()` walk never
77//! reaches `target` (`DisableStatement` defines no `visitExprs`) -- a
78//! `disable label;` statement's own `children().len()` is `0`. The label is
79//! therefore read from the statement's source span in the design's source
80//! text (registered by the caller, [`set_source_text`]), accepted only when
81//! the span reads exactly `disable <identifier> ;` (see `disable_label`).
82//! A named sequential block containing a `disable` gets its own exit block
83//! (`disable_targets`), and `disable blk;` inside it is a jump there, like
84//! `break` (LRM 1800-2023 §9.6.2). In a branch of the named fork block `f`,
85//! `disable f;` stays a statement (codegen: `sv_rt_disable_fork_block`,
86//! ending every branch of the fork) followed by the branch's end. Any other
87//! target -- another process's block, a task, an outer fork, an unreadable
88//! span -- is a named `LoweringError`.
89//!
90//! **Algorithm (PR-007):** [`build_cfg`] is a single recursive-descent walk
91//! of a process body's `Statement` tree (`children()`/`statements()`/
92//! `then_branch()`/`else_branch()`/`body()`/`condition()`/`expr()`) -- an
93//! ordinary CFG builder -- with the ONE addition that entering a `Timed`
94//! node wrapping a `Delay` or `SignalEvent` (posedge/negedge only) timing
95//! control closes the current [`BasicBlock`] with `Terminator::Suspend` and
96//! opens a fresh block for the guarded statement that follows. Every other
97//! suspend-relevant `StatementKind` (`Wait`, `WaitFork`, `WaitOrder`,
98//! `EventTrigger`, `DisableFork`, a fork/join `Block`) and every other
99//! `TimingControlKind` a `Timed` node could wrap (`EventList`,
100//! `ImplicitEvent`, `RepeatedEvent`, `Delay3`, `OneStepDelay`, `CycleDelay`,
101//! `BlockEventList`) is a named [`LoweringError`], never silently treated as
102//! ordinary control flow (PR-006's slice-scope half) -- Task 1 above lifts
103//! this restriction for `Wait`/`EventTrigger`/fork-join `Block`/`EventList`/
104//! `CycleDelay` specifically.
105//!
106//! **Extension policy (PR-069):** [`Terminator`] and [`SuspendPoint`]
107//! intentionally have exactly the variants a task has actually implemented
108//! -- no placeholder variant is ever added ahead of a real implementation,
109//! because an unused variant would force every `match` in this file (and
110//! every future consumer's) to either wildcard it (forbidden by ARCHITECTURE
111//! §7 item 9) or handle a case that can never actually be constructed (a
112//! "gate that cannot fail", forbidden by this wave's correctness posture).
113//! Instead: a task adding a new suspend kind or terminator shape adds the
114//! variant *and* is required to re-audit every `match` site in this file,
115//! since none has a wildcard arm to silently swallow it -- Task 1 did
116//! exactly this for `Loop`/`ForkJoin`/`EdgeList`/`Level`/`CycleDelay` (but
117//! deliberately NOT `Disable` -- see above). Until a shape has a real
118//! implementation, every code path that *would* need it returns a named
119//! [`LoweringError`] instead. `SegTerminator` (PR-041/042/043,
120//! `lower_terminator` below) deliberately keeps its *own* variant set frozen
121//! at `Goto`/`CondGoto`/`Suspend`/`End` -- it is exhaustively matched by
122//! `sv-sim-codegen` (a different track's contract) with no wildcard arm, so
123//! `Terminator::Loop` lowers losslessly to `SegTerminator::Goto` (a real,
124//! already-populated successor block -- `entry` -- always carries the
125//! actual decision, whether that is `header`'s own `Branch` or a plain
126//! `Fallthrough`) rather than adding a new `SegTerminator` variant that
127//! would break that exhaustive match. A `Terminator::ForkJoin` reaching
128//! `lower_terminator` is a caller-discipline precondition violation (see
129//! [`contains_unschedulable_fork_join`]), not a shape `SegTerminator` can
130//! represent at all this wave -- fork/join has no runtime scheduling
131//! semantics yet, so no `SegTerminator` value would be honest here; every
132//! real caller must check that predicate first.
133use crate::symid::SymbolUid;
134use std::collections::HashSet;
135use std::fmt;
136
137use sv_lang::kinds::{ExpressionKind, StatementKind, SymbolKind, TimingControlKind};
138use sv_lang::{
139    EdgeKind, Expression, KnownSystemName, SemNode, Statement, StatementBlockKind, Symbol,
140    TimingControl, UniquePriorityCheck,
141};
142
143/// Every unhandled AST shape this file's builders encounter produces one of
144/// these -- never `panic!`/`unimplemented!`/a silent no-op. Deliberately a
145/// separate type from `sv-sim-codegen::error::LoweringError`: this crate must
146/// never depend on `sv-sim-codegen` (the dependency edge runs the other way
147/// once Task S3c lands), so it cannot reuse that one.
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct LoweringError {
150    pub message: String,
151}
152
153impl LoweringError {
154    // `pub(crate)` (Task S3b deviation, not `fn`-private as Task S3a left
155    // them): `process::liveness` is a sibling module that needs to construct
156    // the same error shape (declared-local width/shape rejections, malformed
157    // block-scope walks) rather than inventing a second `LoweringError`-like
158    // type just for one module boundary. Still crate-private -- no outside
159    // crate constructs one of these directly, only ever sees it through
160    // `Result::Err`.
161    pub(crate) fn unsupported(what: impl Into<String>) -> Self {
162        Self {
163            message: format!("unsupported: {}", what.into()),
164        }
165    }
166
167    pub(crate) fn malformed(what: impl Into<String>) -> Self {
168        Self {
169            message: format!("malformed AST: {}", what.into()),
170        }
171    }
172
173    /// PR-022's hard, typed self-check failure: `process::liveness::
174    /// compute_liveness`'s backward fixpoint computed a live-in symbol that
175    /// `AnalyzedProcedure::read_set()` (or, absent a live `Analysis` handle,
176    /// this crate's own AST-derived read-set aggregate -- see that module's
177    /// doc comment) never reports as read anywhere in the whole procedure.
178    /// Kept as a named constructor on this same flat `LoweringError` shape
179    /// (deviation from the plan's literal `LoweringError::
180    /// LivenessOracleMismatch` enum-variant phrasing: Task S3a already
181    /// committed `LoweringError` as a message-carrying struct with named
182    /// constructors, not an enum, so this follows that established pattern
183    /// rather than introducing a second, incompatible error type mid-crate)
184    /// -- the message names the offending symbol, so a test asserts on
185    /// content, not just `is_err()`, per this file's own established
186    /// discipline.
187    pub(crate) fn liveness_oracle_mismatch(symbol_name: impl Into<String>) -> Self {
188        Self {
189            message: format!(
190                "LivenessOracleMismatch: computed live-in symbol {:?} is absent from the \
191                 whole-procedure read_set -- this is a hard bug in the liveness walk, never \
192                 a legitimate program (PR-022)",
193                symbol_name.into()
194            ),
195        }
196    }
197}
198
199impl fmt::Display for LoweringError {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        write!(f, "{}", self.message)
202    }
203}
204
205impl std::error::Error for LoweringError {}
206
207/// Dense, per-process `ProcessCfg::blocks` index, assigned in AST pre-order
208/// (first-appearance-in-source) allocation order (CG-098, CG-099, PR-114) --
209/// completely independent of any later LLVM basic-block layout order, since
210/// no LLVM `Context`/`Module` exists yet when `build_cfg` runs.
211#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
212pub struct BlockId(pub u32);
213
214/// Dense `Segment` index -- see [`BlockId`]'s doc comment; `lower_to_segments`
215/// assigns `SegId(i)` to the block at index `i`, a 1:1, order-preserving
216/// mapping (PR-041..PR-043).
217#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
218pub struct SegId(pub u32);
219
220/// R-run Task 8 (PR-045/PR-006): a `repeat(N) body;` loop's compiler-
221/// synthesized iteration counter, minted once per `RepeatLoop` statement
222/// [`CfgBuilder::walk_repeat_loop`] decomposes into a real CFG shape (i.e.
223/// only when `body` contains a suspend point somewhere in its own subtree --
224/// the suspend-free case stays the pre-existing opaque leaf `stmt_shared::
225/// lower_repeat_loop` already handles unchanged, see `walk_stmt`'s own
226/// dispatch comment). Unlike a `for` loop's user-visible control variable
227/// (a real `sv_lang::Symbol`, `FrameSlotSource::DeclaredVar`), `repeat`'s
228/// count has no corresponding declared local at all -- there is nothing a
229/// `Branch`'s `cond: Expression` could re-evaluate every iteration the way
230/// `while`/`for` do. This id is this crate's own, `sv_lang`-free handle for
231/// that counter's eventual `FrameSlotSource::SyntheticCounter` frame slot
232/// (`process::liveness`) -- dense per-`ProcessCfg`, assigned in allocation
233/// order (mirrors [`BlockId`]/[`SegId`]'s own numbering discipline), never
234/// reused across two distinct `repeat` statements in the same process (so a
235/// nested or sibling `repeat` gets its own, independent counter slot).
236#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
237pub struct RepeatCounterId(pub u32);
238
239/// The suspend kinds this file implements. `Delay`/`Edge` are PR-006's
240/// slice-scope half (Task S3a); `EdgeList`/`Level`/`CycleDelay` are Task 1's
241/// addition (PR-006's Part 2 half). `ImplicitComb` remains unimplemented --
242/// see the module doc comment's PR-006 accounting.
243#[derive(Debug, Clone)]
244pub enum SuspendPoint<'d> {
245    /// `#<delay>`. The second field is the inlined subroutine whose body holds the delay, when
246    /// one does: a delay's time unit is the unit of the scope that DECLARES it (LRM 3.14.2.1),
247    /// not the caller's.
248    Delay(Expression<'d>, Option<Symbol<'d>>),
249    Edge {
250        signal: Expression<'d>,
251        edge: EdgeKind,
252        /// `SignalEventControl.iffCondition` (LRM `@(posedge clk iff en)`,
253        /// Task 3/CG-170): `None` when no `iff` qualifier is present. The
254        /// kernel's resume prologue for a suspend point carrying `Some(_)`
255        /// ALWAYS re-evaluates it on wake and, if false, immediately
256        /// re-returns `Suspended` with the identical `WaitSpec` unchanged --
257        /// it does not advance the PC and does not re-run the segment body
258        /// (`docs/design/process.md` §2's own "iff re-check" paragraph;
259        /// `compile_iff_guard`'s doc comment in `sv-sim-codegen::
260        /// process_lower` is the codegen half of this same contract).
261        iff: Option<Expression<'d>>,
262    },
263    /// `EventListControl` (`@(a or b or c)`): wake on any member. Deviation
264    /// from `docs/design/process.md` §2's literal `Vec<{edge,signal,iff}>`
265    /// triple -- Task 1's own "Public names/types introduced" text specifies
266    /// the narrower `(Expression, EdgeKind)` pair with no `iff` (matching
267    /// how `Level`'s own `iff`/re-check contract is deferred to Task 3);
268    /// this is the more specific, more recent spec and is followed exactly.
269    ///
270    /// The second field holds each member's `iff` guard (LRM 1800-2023
271    /// 9.4.2.3: `@(posedge a iff c or b)`): always the same length as the
272    /// member list, empty for an unguarded member. The list wakes on a
273    /// member's event only if every one of that member's guards holds then
274    /// (more than one when an `iff` meets an `$inferred_clock` whose own
275    /// clocking event has an `iff`, a conjunction: LRM 17.4, 9.4.2.3).
276    EdgeList(Vec<(Expression<'d>, EdgeKind)>, Vec<Vec<Expression<'d>>>),
277    /// `wait(expr)` (LRM 9.4.3, level-sensitive). Task 3: [`CfgBuilder::
278    /// walk_wait`] wires this as the `else_blk` of a `Terminator::Branch` on
279    /// `expr` itself (never unconditionally) -- "evaluate immediately,
280    /// suspend only if false" (`docs/design/process.md` §2's own "wait(expr)
281    /// special case", CG-169). The per-wake re-check (re-suspending
282    /// unchanged on a spurious wake, CG-115) is entirely codegen's job
283    /// (`sv-sim-codegen::process_lower::compile_iff_guard`'s resume
284    /// prologue) -- this crate never evaluates `expr`.
285    Level(Expression<'d>),
286    /// `##N` (clocking-block-relative cycle delay) -- **Task 6 (CG-171)
287    /// revision:** no longer a bare cycle count. ARCHITECTURE §6.5 represents
288    /// `CycleDelayControl` as "kept as an unresolved expression + ClockDomain
289    /// for the scheduler to convert cycles->ticks at runtime", i.e. codegen
290    /// must never precompute an absolute tick offset (a clock's period can
291    /// change dynamically -- see `sched::mod::EventTerm::Repeat`'s own SC-053
292    /// doc comment for the differentiating behavior this rules out). This
293    /// variant carries `count` (the compile-time-constant cycle count --
294    /// still restricted the same way `##N` always was, see
295    /// [`CfgBuilder::walk_timed`]'s `CycleDelay` arm) plus the syntactically
296    /// inferred single clock this process suspends against, `clock`/
297    /// `edge` (or, with a default clocking block in scope, that block
298    /// itself with `BothEdges`: `##N` counts its events) -- the SAME `inferred_clock` this file already tracked before
299    /// this task (previously computed then silently discarded once
300    /// ambiguity-checked; now threaded through as real payload). Resolving
301    /// `clock` to a concrete `StateCellId` still happens at codegen
302    /// time (`sv-sim-codegen::process_lower::resolve_suspend_point`, exactly
303    /// like `Edge`'s own signal) -- what does NOT happen at codegen time is
304    /// converting `count` cycles into an absolute tick number: that stays a
305    /// live, per-edge `EventTerm::Repeat{count, inner: Edge{cell, edge}}`
306    /// wait the SCHEDULER counts down at runtime (already built and tested,
307    /// SC-052/SC-053), so a clock whose period later changes still produces
308    /// the correct absolute timing by construction, with no cached tick
309    /// offset to go stale.
310    ///
311    /// **Deviation from the plan's literal `analysis::ClockDomainId`-carrying
312    /// wire design, and a real, verified SCOPE LIMIT (both recorded in full
313    /// in this task's deviations -- not silently narrowed):** the plan's own
314    /// text describes resolving `clock` against a live `sv-lang`-
315    /// `Analysis`-backed WOCB partition result (`analysis::
316    /// StaticAnalysisResult::clock_domains()`) threaded all the way from
317    /// `sv-sim-bin::run`'s elaboration pipeline through to the scheduler's
318    /// `Engine`. That plumbing does not exist anywhere in this tree yet
319    /// (`analysis::analyze()` is never called from `run.rs`/`aot.rs`/
320    /// `pipeline.rs` today), and building it is a genuinely separate,
321    /// `run.rs`-touching undertaking this task's own binding rules forbid
322    /// risking (wave-B-runtime is concurrently landing in that exact file).
323    /// This variant instead reuses the clock inference this crate ALREADY
324    /// performed for every `##N` before this task (the same
325    /// `inferred_clock` ambiguity check, now threaded through as real
326    /// payload instead of discarded) -- **which is genuinely narrower than
327    /// "the process's own inferred clock domain," not merely a smaller-but-
328    /// equivalent instance of it, and this narrowing is load-bearing, not
329    /// cosmetic:** it resolves `##N` written inside a process that ALSO
330    /// itself syntactically sights a `posedge`/`negedge` edge somewhere
331    /// earlier (the common `always_ff @(posedge clk) ... ##N ...` idiom),
332    /// but does NOT resolve a `##N` relative to a `default clocking`/named
333    /// `clocking cb @(posedge clk); endclocking` block referenced from a
334    /// process with NO local edge sighting of its own -- verified directly
335    /// (a throwaway test compiling ARCHITECTURE §6.5's own canonical
336    /// clocking-block fixture, `default clocking cb; ... initial ##3 x=1;`,
337    /// with no `@(posedge clk)` anywhere in that second `initial` block):
338    /// `walk_timed`'s `CycleDelay` arm returns the SAME "no unambiguous
339    /// single inferred clock in scope" `LoweringError` for it that a
340    /// genuinely unclocked `##N` gets, because `default clocking`/
341    /// `ClockingBlock` declarations are never read by this mechanism at
342    /// all. Reading them is also a `session.rs`-owned, pre-existing,
343    /// verified-separately-blocked gap regardless (`sv-sim-codegen::
344    /// session.rs`'s own structural-symbol classification already places
345    /// `SymbolKind::ClockingBlock` in its "no correct lowering exists yet"
346    /// bucket -- a design containing one fails upstream of this crate
347    /// entirely), so closing it is not a smaller extension of this task's
348    /// own mechanism, it needs a different, cross-track-blocked one. CG-171
349    /// therefore closes for real, common `##N` usage (a clock edge
350    /// syntactically visible in the SAME process) but stays open for the
351    /// `default clocking`-bound shape -- see this task's deviations for the
352    /// full accounting, and `crates/sv-sim-harness/tests/
353    /// slice_vs_iverilog.rs::clocking_cycle_delay_matches_iverilog_bit_for_bit`
354    /// for the still-`#[ignore]`d e2e proof (updated by this task to name
355    /// this exact, narrower residual, not the stale "CG-171 unimplemented"
356    /// reason it carried before). See `sched::reschedule_req_proposal::
357    /// KIND_REPEAT_EDGE`'s own doc comment for the wire shape the resolved
358    /// (local-edge) case resolves into -- a single-cell, single-edge repeat
359    /// wait that fits the frozen 24-byte budget inline (no out-of-line
360    /// table needed, unlike `EdgeList`/`ImplicitComb`).
361    ///
362    /// **Verified, separately-recorded finding (not this task's bug to fix,
363    /// but load-bearing for why no NEW oracle-diffed e2e fixture proves this
364    /// closure end-to-end):** `##N` outside a `default clocking` context is
365    /// not accepted as valid SystemVerilog by either reference tool this
366    /// crate uses -- `iverilog -g2012` rejects a bare `##2` inside an
367    /// ordinary `always_ff` body with a flat parser "syntax error", and
368    /// `verilator --binary -sv` rejects the identical construct with
369    /// "Usage of cycle delays requires default clocking (IEEE 1800-2023
370    /// 14.11)" (both verified directly, by hand, against a throwaway
371    /// fixture). This mechanism's "syntactically inferred single clock"
372    /// acceptance criterion PRE-DATES this task (an earlier task's own
373    /// `inferred_clock` field, which this task only threads through as real
374    /// payload instead of discarding) -- this task neither introduces nor
375    /// revisits that acceptance decision, only documents, empirically, that
376    /// no available reference oracle can differentially validate it end to
377    /// end: the codegen-level JIT+run round trip (`crates/sv-sim-codegen/
378    /// tests/frame_roundtrip_matrix.rs`) is therefore the strongest
379    /// available proof of this specific closure, not an iverilog/Verilator
380    /// diff.
381    CycleDelay {
382        count: u32,
383        /// The clock: a signal (the process's inferred clock, counted on
384        /// `edge`), or the default clocking block (its event counter,
385        /// counted on any change).
386        clock: Symbol<'d>,
387        edge: EdgeKind,
388        /// A clocking drive THROUGH a virtual interface (`v.sb.c <= ##1 1`,
389        /// LRM 25.9.1): the clock variable's member access. `clock` is then
390        /// the template clocking block, which has no signal; the run time
391        /// counts the events of the clocking block of the instance `v` holds.
392        via: Option<Expression<'d>>,
393    },
394    /// **Task 6 (CG-081):** `ImplicitEventControl` (`@*`/`@(*)`). Fieldless
395    /// per `docs/design/process.md` §2's own `SuspendPoint` sketch would
396    /// suggest (`WaitSpec::ImplicitComb(SubscriptionSetId)` is resolved
397    /// against a separate table, not embedded read-set data) -- deviates by
398    /// carrying the guarded body's own AST-derived read-set directly
399    /// (`Vec<Expression<'d>>`, computed by [`implicit_comb_read_set`]) rather
400    /// than being truly fieldless, because this crate's `build_cfg` has no
401    /// live `sv_lang::Analysis`/`AnalyzedProcedure` handle to later re-derive
402    /// it from at codegen time (see [`implicit_comb_read_set`]'s own doc
403    /// comment for the full reasoning) -- the read-set has to be captured
404    /// HERE, while the guarded body is in hand, or nowhere in this crate.
405    ImplicitComb(Vec<Expression<'d>>),
406    /// **Task 6 (CG-081):** `OneStepDelayControl` (the LRM's built-in
407    /// `1step` delay, one global-precision tick, SC-123). Fieldless -- the AST node itself carries no
408    /// expression (confirmed against the vendored slang C++:
409    /// `OneStepDelayControl` declares no field and no `visitExprs` override
410    /// at all, so `TimingControl::children()` is always empty for this
411    /// kind). Resolved at codegen time to `Suspend::Delay(1)` (LRM 3.14.3: a
412    /// step is the global time precision) -- see
413    /// `sv-sim-codegen::process_lower::resolve_suspend_point`'s `OneStep`
414    /// arm.
415    OneStep,
416    /// `wait fork;` (LRM 9.6.1, SC-125): resume once every child this process
417    /// forked has finished. Payload-free -- the scheduler tracks the children.
418    WaitFork,
419    /// Classes plan Task 13 (LRM 9.7): `p.await();` / `p.suspend();` as a
420    /// statement. The call itself queues a process request; this suspend
421    /// yields so the scheduler applies it before the next statement runs
422    /// (`Suspend::ByRequest`). Payload-free.
423    ByRequest,
424}
425
426/// Task 7 (PR-116, XC-044): a fieldless, `'static`-safe mirror of
427/// [`SuspendPoint`]'s discriminants. `SuspendPoint<'d>` itself cannot be
428/// named `&'static [SuspendPoint]` -- every payload-carrying variant embeds
429/// a borrowed `sv_lang::Expression<'d>`, which is only ever alive for the
430/// lifetime of a real, already-compiled `Design`, so no `'static` instance
431/// of it can exist independent of one. This type is exactly the
432/// "which shape" tag [`suspending_statement_kinds`]'s two callers (the
433/// frame-slot round-trip matrix in `sv-sim-sim`'s own tests and the JIT+run
434/// matrix in `sv-sim-codegen`'s tests) both need, without requiring either
435/// to fabricate a borrowed `Expression` from nothing.
436#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
437pub enum SuspendPointKind {
438    Delay,
439    Edge,
440    EdgeList,
441    Level,
442    CycleDelay,
443    /// Task 6 (CG-081): `ImplicitEventControl` (`@*`/`@(*)`).
444    ImplicitComb,
445    /// Task 6 (CG-081): `OneStepDelayControl` (`#0`).
446    OneStep,
447    /// `wait fork;` (SC-125).
448    WaitFork,
449    /// `process::await()` / `process::suspend()` (Classes plan Task 13).
450    ByRequest,
451}
452
453impl<'d> SuspendPoint<'d> {
454    /// Exhaustive, no-wildcard match (ARCHITECTURE §7 item 9): if a future
455    /// task adds a new `SuspendPoint` variant without extending
456    /// [`SuspendPointKind`] and this match, the crate fails to COMPILE, not
457    /// merely under-tests -- a stronger guarantee than
458    /// [`suspending_statement_kinds`]'s own length alone could give.
459    pub fn kind(&self) -> SuspendPointKind {
460        match self {
461            SuspendPoint::Delay(..) => SuspendPointKind::Delay,
462            SuspendPoint::Edge { .. } => SuspendPointKind::Edge,
463            SuspendPoint::EdgeList(..) => SuspendPointKind::EdgeList,
464            SuspendPoint::Level(_) => SuspendPointKind::Level,
465            SuspendPoint::CycleDelay { .. } => SuspendPointKind::CycleDelay,
466            SuspendPoint::ImplicitComb(_) => SuspendPointKind::ImplicitComb,
467            SuspendPoint::OneStep => SuspendPointKind::OneStep,
468            SuspendPoint::WaitFork => SuspendPointKind::WaitFork,
469            SuspendPoint::ByRequest => SuspendPointKind::ByRequest,
470        }
471    }
472}
473
474/// Task 7 (PR-116, XC-044): every `SuspendPoint` kind this file's CFG
475/// builder currently gives a real shape to (i.e. every variant
476/// [`SuspendPoint`] itself has -- see that enum's own doc comment for the
477/// still-open `ImplicitEvent`/`RepeatedEvent`/`Delay3`/`OneStepDelay`/
478/// `BlockEventList` gap PR-069's extension policy keeps out of the enum
479/// entirely). One definition, promoted from a test-only helper to real
480/// production code specifically so both `sv-sim-sim`'s own meta-test and
481/// `sv-sim-codegen`'s JIT+run matrix enumerate the identical list, never two
482/// independently-maintained copies that could silently drift apart (this
483/// project's own "duplicate pure-function eval diverges" lesson).
484///
485/// **Not every kind in this list is yet resolvable by
486/// `sv-sim-codegen::process_lower::resolve_suspend_point`** --
487/// `EdgeList`/`CycleDelay` are real CFG shapes (this file's own Task 1) but
488/// still a named, codegen-side `LoweringError` ("this SuspendPoint kind has
489/// no codegen-side resolution yet"), a pre-existing, documented scope
490/// boundary from that same task, not something this row's own file list
491/// (`crates/sv-sim-sim/tests/frame_roundtrip_matrix.rs`, `crates/
492/// sv-sim-codegen/tests/frame_roundtrip_matrix.rs`, this file) can close
493/// without expanding into `sv-sim-codegen::process_lower`'s own `Files`
494/// scope. `sv-sim-codegen`'s own round-trip matrix asserts the SPECIFIC,
495/// named rejection for these two kinds (never silently skipping them), so
496/// the gate still fails loudly in both directions: if a kind that should
497/// round-trip stops doing so, or if a kind that should still be rejected
498/// silently starts "succeeding" with an unreviewed codegen change.
499pub fn suspending_statement_kinds() -> &'static [SuspendPointKind] {
500    &[
501        SuspendPointKind::Delay,
502        SuspendPointKind::Edge,
503        SuspendPointKind::EdgeList,
504        SuspendPointKind::Level,
505        SuspendPointKind::CycleDelay,
506        SuspendPointKind::ImplicitComb,
507        SuspendPointKind::OneStep,
508        SuspendPointKind::WaitFork,
509        SuspendPointKind::ByRequest,
510    ]
511}
512
513/// A `BasicBlock`'s single exit (PR-008, extended by Task 1).
514#[derive(Debug, Clone)]
515pub enum Terminator<'d> {
516    /// Falls straight into `BlockId` with no branch/suspend (e.g. the merge
517    /// point after an `if`/`else`).
518    Fallthrough(BlockId),
519    /// Goes to `arms[i]` when `on` evaluates to `i`, else to `default_blk`
520    /// -- reporting an `$error` at source offset `report` first, when set
521    /// (a `wait_order` with no `else`).
522    Switch {
523        on: SwitchOn<'d>,
524        arms: Vec<BlockId>,
525        default_blk: BlockId,
526        report: Option<sv_lang::SourceLoc>,
527    },
528    /// `if`/`case` (PR-044 -- expanded to a br-chain at the `Terminator`
529    /// level; the ternary operator `?:` never appears here, only inside a
530    /// straight-line expression within a [`BasicBlock`]'s `stmts`).
531    Branch {
532        cond: Expression<'d>,
533        then_blk: BlockId,
534        else_blk: BlockId,
535    },
536    /// An immediate assertion (LRM 16.3) whose pass or fail statement waits,
537    /// so the statement cannot stay a straight-line leaf. Goes to `pass_blk`
538    /// (`fail_blk`) when the expression is true (not true) and the assertion
539    /// is checking with that action on (20.11), else to `done_blk`; the
540    /// default `$error` of an assert/assume with no fail statement is
541    /// reported inline before `done_blk`.
542    Assertion {
543        stmt: Statement<'d>,
544        pass_blk: BlockId,
545        fail_blk: BlockId,
546        done_blk: BlockId,
547    },
548    Suspend {
549        point: SuspendPoint<'d>,
550        resume_blk: BlockId,
551    },
552    /// PR-048: `loop_back` is `None` for `initial`/`final` procedures and
553    /// `Some(entry)` for `always`/`always_ff`/`always_comb`/`always_latch`
554    /// procedures -- see [`ProcessLoopKind`].
555    End { loop_back: Option<BlockId> },
556    /// PR-045 (Task 1): `for`/`while`/`do-while`/`forever`/`foreach`.
557    /// `header`'s own terminator (a plain `Branch` for a real per-iteration
558    /// condition, or `Fallthrough` when there is none -- `forever`) carries
559    /// the actual per-iteration decision, and `body`'s own terminator closes
560    /// the back edge to `header`. `entry` is the block `cur` (the one this
561    /// `Terminator::Loop` closes) actually transfers control to on first
562    /// entry -- `header` for `while`/`for`/`forever`/`foreach` (the
563    /// condition, if any, is checked before the first iteration), but
564    /// `body` for `do-while` (the body always runs once before any check) --
565    /// a deviation from `docs/design/process.md` §2's literal 3-field
566    /// `Loop{header,body,exit}` (recorded in this task's deviations): a
567    /// `do-while`'s real entry edge genuinely cannot be recovered from
568    /// `header`/`body`/`exit` alone once `header` is required (as this
569    /// task's own tests require) to remain a distinct, inspectable
570    /// post-body recheck block rather than being aliased to `body`. Codegen
571    /// may fully unroll a static-bound `for`, but the CFG representation
572    /// itself always has a real `Loop` terminator so the liveness/segment-
573    /// lowering passes never special-case "was this unrolled" (this file's
574    /// own `lower_terminator` maps it losslessly to
575    /// `SegTerminator::Goto(entry)`, since whichever real block `entry`
576    /// names already carries its own correct terminator). `foreach`'s
577    /// per-dimension bound is a `ConstantRange`, not an `Expression` slang
578    /// exposes anywhere on the statement -- see `walk_foreach_loop`'s own
579    /// doc comment for why that one kind's `body` is a chain of unrolled
580    /// copies rather than a real back edge (a second, separately-recorded
581    /// deviation).
582    Loop {
583        header: BlockId,
584        body: BlockId,
585        exit: BlockId,
586        entry: BlockId,
587    },
588    /// `fork ... join`/`join_any`/`join_none` (Task 1, PR-069/docs/design/
589    /// process.md §7): construction only, never scheduled. Each
590    /// `branches[i]` is a full nested [`ProcessCfg`] (a fork branch can
591    /// contain its own suspends). See [`contains_unschedulable_fork_join`]
592    /// -- a real caller must check this before calling [`lower_to_segments`]
593    /// on a CFG that might contain one.
594    ForkJoin {
595        kind: StatementBlockKind,
596        branches: Vec<ProcessCfg<'d>>,
597        join_blk: BlockId,
598    },
599    /// R-run Task 8 (PR-045/PR-006): a `repeat(N) body;` loop's `header`
600    /// terminator, when `body` contains a suspend point (see
601    /// [`RepeatCounterId`]'s own doc comment for why this is a distinct
602    /// shape from `Branch`, never a fabricated `cond: Expression`). Mirrors
603    /// `while`'s own `header`/`Branch` role exactly -- `then_blk` is the
604    /// loop body, `else_blk` is the loop's exit -- except the re-checked
605    /// condition is `counter != 0` against this loop's own compiler-
606    /// synthesized [`RepeatCounterId`] slot, not a re-evaluated user
607    /// expression.
608    RepeatCheck {
609        counter: RepeatCounterId,
610        then_blk: BlockId,
611        else_blk: BlockId,
612    },
613    /// A `case`/`casez`/`casex`/`case ... inside` statement whose arms
614    /// cannot stay one straight-line leaf (an arm suspends, or `break`s /
615    /// `continue`s an enclosing process loop, or `return`s from an inlined
616    /// task): `stmt` supplies the selector and labels, matched in item
617    /// order, `arms[i]` is `stmt.case_items()[i]`'s block and `default_blk`
618    /// runs when no item matches. Also a `unique`/`unique0`/`priority` `if`
619    /// series: `stmt` is its first `if` ([`if_chain`]), `arms[i]` the
620    /// then-statement of the i-th condition and `default_blk` the final `else`,
621    /// so the dispatch can report a violation where the series executes.
622    /// Also an `if` with a pattern-matching or `&&&`-chained predicate (one
623    /// arm, [`is_clause_if`]) and a `case ... matches` (`arms[i]` is
624    /// `pattern_case_items()[i]`'s block, `default_blk` its `default`).
625    Case {
626        stmt: Statement<'d>,
627        arms: Vec<BlockId>,
628        default_blk: BlockId,
629    },
630}
631
632/// One argument transfer of a task call inlined into the process CFG
633/// (`CfgBuilder::inline_task_call`): a task whose body can suspend (`@`, `#`,
634/// `wait`, ...) cannot be an ordinary compiled function, so its body is
635/// walked in place and these run at the start of the inlined region's
636/// blocks. Every symbol named here is one of `ProcessCfg::inlined_locals`.
637#[derive(Debug, Clone)]
638pub enum ArgBinding<'d> {
639    /// `formal = actual`: an input (or the copy-in of an inout) formal.
640    In {
641        formal: Symbol<'d>,
642        actual: Expression<'d>,
643    },
644    /// `var` = its declaration initializer, or its type's default: an
645    /// `output` formal or one of the task's own locals, at task entry.
646    Init { var: Symbol<'d> },
647    /// `actual = formal`: an output/inout formal's copy-back after the body
648    /// (`actual` is the lvalue expression).
649    Out {
650        actual: Expression<'d>,
651        formal: Symbol<'d>,
652    },
653    /// `var = value`: a `foreach` loop variable's starting value.
654    Const { var: Symbol<'d>, value: i64 },
655    /// `var` = the current object: the `this` of a class-method task inlined
656    /// for an implicit-`this` call (`run()` inside another method), bound in
657    /// the caller's context. An explicit receiver (`obj.run()`) binds through
658    /// [`ArgBinding::In`] instead.
659    ThisOf { var: Symbol<'d> },
660    /// An inlined task's input formal or entry-initialized local ending
661    /// with the call (an automatic variable ends with its scope, LRM 6.21):
662    /// whatever class / collection handles it holds are released (LRM
663    /// 13.5.1). Runs at the call's exit block, so `return` and `disable`
664    /// release too.
665    Release { var: Symbol<'d> },
666    /// `var += by`: a `foreach` loop variable's per-iteration step.
667    Step { var: Symbol<'d>, by: i64 },
668    /// `void'(arr.first(var))`, or with `next` `void'(arr.next(var))`: a
669    /// `foreach` over the associative array `arr` walking its keys.
670    ///
671    /// For an associative array nested in a collection (`int m[int][string]`,
672    /// `int q[$][int]`), `path` holds the enclosing `foreach` levels that
673    /// select the array from `arr` (as [`RepeatCount::NestedSize`]); empty
674    /// for `arr` itself.
675    AssocKey {
676        var: Symbol<'d>,
677        arr: Expression<'d>,
678        path: Vec<NestStep<'d>>,
679        next: bool,
680    },
681    /// `lhs = #d rhs;` and the other intra-assignment timing controls (LRM
682    /// 1800-2023 §10.4.1): the right-hand side is evaluated before the wait
683    /// into the compiler temporary `temp` (a `FrameSlotSource::Temporary`
684    /// frame slot, sized by [`ProcessCfg::captures`]`[temp]`)...
685    Capture { temp: u32, value: Expression<'d> },
686    /// ...and assigned to its target after it.
687    ///
688    /// With `nba` (`lhs <= @(e) rhs;`, LRM 1800-2023 §10.4.2) the assignment
689    /// is a nonblocking one, staged for the NBA region: it runs in the
690    /// synthetic fork branch [`CfgBuilder::walk_nba_event`] spawns.
691    AssignCaptured {
692        temp: u32,
693        target: Expression<'d>,
694        nba: bool,
695    },
696    /// `formal = temp`: a recursive task's input formal, copied from the
697    /// call's evaluated argument ([`InheritedSlot::Temp`]) as the forked
698    /// activation starts ([`CfgBuilder::inline_recursive_task`]).
699    FromCaptured { temp: u32, formal: Symbol<'d> },
700    /// `temp = formal`: a recursive task's `output`/`inout` formal, parked in
701    /// a compiler temporary as the activation ends so the spawner can copy
702    /// it back ([`InheritedSlot::Return`]).
703    ToCaptured { temp: u32, formal: Symbol<'d> },
704    /// Marks capture temporary `temp` as the value of the inlined call `call`
705    /// of a function that spawns processes: the call's expression reads the
706    /// temporary (keyed by [`call_result_key`]) instead of the function's
707    /// shared result variable, which a second call of the same function in
708    /// the statement, or another override of a virtual method, overwrites.
709    /// A no-op at run time; [`ArgBinding::ToCaptured`] parks the value.
710    CallResult { temp: u32, call: Expression<'d> },
711    /// The resume of a mixed `@(class_event or signal ...)` wait (LRM 1800-2023
712    /// 9.4.2.1, 15.5): the process leaves the class-event queues it is still
713    /// queued on (the signal side woke it), so a later trigger of one of those
714    /// events cannot wake a wait it is no longer in.
715    ClassEventLeave,
716    /// `temp` = the caller's current object: the receiver slot of a recursive
717    /// class-method activation called with an implicit `this` (LRM 1800-2023
718    /// 8.11, 13.4.1); an explicit receiver is an ordinary
719    /// [`ArgBinding::Capture`].
720    CaptureThis { temp: u32 },
721    /// A `ref` formal bound to element `actual = c[i]` of a queue or dynamic
722    /// array (LRM 1800-2023 13.5.2, 7.10.3): opens a runtime reference to the
723    /// element into the 64-bit temporary `temp` (the collection's tracking
724    /// outdates it on delete / pop / clear / resize), then `formal` = the
725    /// element.
726    RefOpen {
727        temp: u32,
728        formal: Symbol<'d>,
729        actual: Expression<'d>,
730    },
731    /// The copy-out of [`ArgBinding::RefOpen`]: `actual` = `formal` unless
732    /// the reference `temp` was outdated meanwhile.
733    RefClose {
734        temp: u32,
735        formal: Symbol<'d>,
736        actual: Expression<'d>,
737    },
738}
739
740/// A frame slot a spawned branch copies by value from its parent
741/// ([`ProcessCfg::inherited`]).
742#[derive(Debug, Clone, Copy, PartialEq, Eq)]
743pub enum InheritedSlot {
744    /// Capture temporary `parent` of the forking process -> `child` of the branch.
745    Temp { parent: u32, child: u32 },
746    /// Repeat counter `parent` -> `child`.
747    Counter {
748        parent: RepeatCounterId,
749        child: RepeatCounterId,
750    },
751    /// The other way round, when the branch is done: its capture temporary
752    /// `child` -> temporary `parent` of the process that forked it (a
753    /// recursive task's `output`/`inout` formals, LRM 1800-2023 13.5).
754    Return { parent: u32, child: u32 },
755}
756
757/// What a [`Terminator::Switch`] selects on.
758#[derive(Clone)]
759pub enum SwitchOn<'d> {
760    /// The index of the wait term that woke the process at the suspend just
761    /// before (`sv_rt_fired_term`): `wait_order`.
762    FiredTerm,
763    /// A virtual interface's instance id minus one (`netlist::vif`): an
764    /// interface task called through it.
765    VifId(Expression<'d>),
766    /// A `randcase`'s item, picked at random by the items' weights (none
767    /// when every weight is 0).
768    RandCase(Statement<'d>),
769    /// The arm whose class template ids hold the dynamic class of the object
770    /// `receiver` names (the current object when `None`): a virtual class
771    /// task that can suspend, one inlined override per arm (LRM 8.20).
772    ObjClass {
773        receiver: Option<Expression<'d>>,
774        ids: Vec<Vec<u32>>,
775    },
776    /// No arm is ever taken: the switch only reports its error (`what` is the
777    /// message) and continues at the default -- a call to an `extern forkjoin`
778    /// task no module implements (LRM 25.7.5).
779    NoTarget { what: String },
780    /// The arm of the named block (`labels[i]`, innermost first) that the
781    /// subroutine call just before disabled with `disable <block>;` from
782    /// inside a function (LRM 9.6.2, 35.9), else the default
783    /// (`sv_rt_dpi_fn_disable_index`).
784    FnDisable { labels: Vec<String> },
785}
786
787impl std::fmt::Debug for SwitchOn<'_> {
788    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
789        match self {
790            SwitchOn::NoTarget { what } => write!(f, "NoTarget({what})"),
791            SwitchOn::FnDisable { labels } => write!(f, "FnDisable({labels:?})"),
792            SwitchOn::FiredTerm => write!(f, "FiredTerm"),
793            SwitchOn::VifId(e) => write!(f, "VifId({:?})", e.kind()),
794            SwitchOn::RandCase(_) => write!(f, "RandCase"),
795            SwitchOn::ObjClass { ids, .. } => write!(f, "ObjClass({ids:?})"),
796        }
797    }
798}
799
800impl<'d> SwitchOn<'d> {
801    /// Every expression the selection reads.
802    pub fn reads(&self) -> Vec<Expression<'d>> {
803        match self {
804            SwitchOn::FiredTerm | SwitchOn::NoTarget { .. } | SwitchOn::FnDisable { .. } => {
805                Vec::new()
806            }
807            SwitchOn::VifId(e) => vec![*e],
808            SwitchOn::RandCase(s) => s.randcase_items().iter().map(|i| i.expr()).collect(),
809            SwitchOn::ObjClass { receiver, .. } => receiver.iter().copied().collect(),
810        }
811    }
812}
813
814/// One enclosing `foreach` dimension on the way to a nested collection
815/// ([`RepeatCount::NestedSize`]): its index variable and, for a fixed-size
816/// dimension, its `(left, right)` bounds (`None`: a queue / dynamic array
817/// dimension, indexed from 0).
818#[derive(Debug, Clone, Copy)]
819pub struct NestStep<'d> {
820    pub var: Symbol<'d>,
821    pub fixed: Option<(i32, i32)>,
822    /// The dimension is an associative array: `var` holds a key, not a
823    /// position.
824    pub assoc: bool,
825}
826
827/// A counted loop's iteration count: a `repeat (N)` loop's count
828/// expression, a `foreach` loop's static element count, or the number of
829/// elements a queue or dynamic array has when its `foreach` starts.
830#[derive(Debug, Clone)]
831pub enum RepeatCount<'d> {
832    Expr(Expression<'d>),
833    Const(u64),
834    /// The size of this queue / dynamic array expression.
835    Size(Expression<'d>),
836    /// The size of the queue / dynamic array reached from `arr` by the
837    /// enclosing `foreach` indices `path` (`a[i][j]` of `foreach (a[i, j,
838    /// k])` over `int a[2][3][]`), read when the loop is entered.
839    NestedSize {
840        arr: Expression<'d>,
841        path: Vec<NestStep<'d>>,
842    },
843    /// A count the forking process already evaluated: the counter slot is
844    /// copied in at the spawn ([`InheritedSlot::Counter`]), never
845    /// initialized here. `0` is the count expression, which sizes the slot.
846    Inherited(Expression<'d>),
847}
848
849impl<'d> RepeatCount<'d> {
850    /// The expression the count is computed from, if the count is not a
851    /// constant (for `Size`, the collection).
852    pub fn expr(&self) -> Option<Expression<'d>> {
853        match self {
854            RepeatCount::Expr(e) | RepeatCount::Size(e) => Some(*e),
855            RepeatCount::NestedSize { arr, .. } => Some(*arr),
856            RepeatCount::Const(_) | RepeatCount::Inherited(_) => None,
857        }
858    }
859}
860
861/// A straight-line run of non-suspending statements ending in exactly one
862/// [`Terminator`] (PR-008).
863#[derive(Debug, Clone)]
864pub struct BasicBlock<'d> {
865    /// Inlined-task argument transfers, run before `stmts` (see
866    /// [`ArgBinding`]).
867    pub bindings: Vec<ArgBinding<'d>>,
868    /// The `this` of the innermost class-method task inlined at this block
869    /// (its body's blocks; its binding and continuation blocks belong to the
870    /// caller): what an implicit-`this` member access or method call in the
871    /// block resolves against when the process itself is not a method.
872    pub this_var: Option<Symbol<'d>>,
873    /// The `ref` formals of the tasks inlined at this block that name a
874    /// static variable or net of their call (LRM 13.5.2), with that storage:
875    /// the formal reads and writes the actual itself in this block, so one
876    /// process may bind the same formal to different actuals per call.
877    pub ref_aliases: Vec<(Symbol<'d>, Symbol<'d>)>,
878    pub stmts: Vec<Statement<'d>>,
879    /// VPI `cbStmt` probe points (IEEE 1800-2023 38.36.1): `(i, s)` means the
880    /// statement `s` is about to execute and the probe runs just before
881    /// `stmts[i]` (`i == stmts.len()` is after the last one, still before
882    /// `step_exprs` and the terminator). Only statements this CFG decomposes
883    /// (or keeps as a leaf, which codegen then does not probe again) appear
884    /// here; codegen emits a probe for them only when a VPI library is loaded.
885    pub probes: Vec<(usize, Statement<'d>)>,
886    /// VPI `cbDisable` probe points (38.36.1): `(i, s)` means the `disable`
887    /// statement `s`, which this CFG turned into a jump out of the named block
888    /// it names (so it is not in `stmts`), has taken effect; the probe runs
889    /// where `probes` at index `i` do, after them.
890    pub disable_probes: Vec<(usize, Statement<'d>)>,
891    /// VPI frame probe points (37.43) of the blocking tasks inlined into this
892    /// CFG: they run where `probes` at the same index do, after them (and
893    /// after any `disable_probes`).
894    pub frame_probes: Vec<(usize, FrameProbe<'d>)>,
895    /// Covergroup block events (LRM 19.3 `@@(begin t)` / `@@(end t)`) of the
896    /// blocking tasks inlined into this CFG: `(i, task, is_end)` signals the
897    /// begin (or end) of `task`'s activation where `probes` at index `i` run.
898    /// Codegen emits only those whose task a covergroup names.
899    pub block_events: Vec<(usize, Symbol<'d>, bool)>,
900    /// `(i, subroutine)`: `stmts[i]` was written in the body of the inlined `subroutine`, whose
901    /// time unit its delays (`q <= #d v`) are in (LRM 3.14.2.1). Absent for the process's own.
902    pub stmt_scopes: Vec<(usize, Symbol<'d>)>,
903    /// A `for` loop's per-iteration step expressions (e.g. `i++`), run after
904    /// `stmts` and before `terminator` fires (Task 1, `walk_for_loop`).
905    /// Empty for every block that is not a `for` loop's step block. A
906    /// separate field, not folded into `stmts: Vec<Statement<'d>>`, because
907    /// `Statement::for_loop_steps()` returns bare `Expression<'d>`s (`i++`
908    /// is an expression in this grammar position, never a `Statement` --
909    /// confirmed against the pinned `sv-lang`=0.1.9 binding, which has no
910    /// expression-to-statement wrapper) and this crate never fabricates AST
911    /// nodes. Every slot this task's `for` fixture writes through
912    /// (`i` itself) is a `FrameSlotSource::DeclaredVar` permanent slot
913    /// (`lower_to_segments`'s own doc comment), so omitting these from
914    /// `liveness::compute_liveness`'s backward read/write walk does not
915    /// under-approximate any frame-slot's live range -- codegen consumption
916    /// of this field is future work, tracked in this task's deviations, not
917    /// a silent drop (the expressions are still here, in the CFG, for
918    /// whichever pass reads `step_exprs` next).
919    pub step_exprs: Vec<Expression<'d>>,
920    /// R-run Task 8: `Some((counter, count_expr))` iff this block is a
921    /// suspend-containing `repeat(N)` loop's own entry block -- `count_expr`
922    /// (`RepeatLoop::condition()`, per LRM the loop's iteration COUNT, not a
923    /// boolean) is evaluated exactly ONCE, run after `stmts` and before
924    /// `terminator` fires (mirrors [`Self::step_exprs`]'s own "run after
925    /// stmts, before terminator" placement), its result (clamped to
926    /// non-negative) becoming `counter`'s starting value. `None` for every
927    /// other block, including a suspend-free `repeat` (which stays the
928    /// pre-existing opaque leaf `stmt_shared::lower_repeat_loop` already
929    /// handles -- see [`RepeatCounterId`]'s own doc comment).
930    pub repeat_init: Option<(RepeatCounterId, RepeatCount<'d>)>,
931    /// R-run Task 8: `Some(counter)` iff this block is a suspend-containing
932    /// `repeat(N)` loop's body's own back-edge block -- decrements
933    /// `counter` by exactly one, run after `stmts`/`step_exprs` and before
934    /// `terminator` fires. `None` for every other block.
935    pub repeat_decrement: Option<RepeatCounterId>,
936    /// `true` iff this block is the one an `expect` statement's wait resumes
937    /// into (LRM 1800-2023 §16.17): its first statement is that `expect` again,
938    /// whose resume half (the pass/`else` action, selected by the outcome the
939    /// process was woken with) codegen lowers there.
940    pub expect_resume: bool,
941    pub terminator: Terminator<'d>,
942}
943
944/// PR-004, PR-008: the CFG built by [`build_cfg`] -- one LLVM function per
945/// process (Task S3c) will eventually `switch` on `resume_pc` with one case
946/// per segment derived from `blocks` below.
947#[derive(Debug, Clone)]
948pub struct ProcessCfg<'d> {
949    pub blocks: Vec<BasicBlock<'d>>,
950    pub entry: BlockId,
951    /// The process body's original root `Statement` (Task S3b deviation,
952    /// not in Task S3a's original field list): `process::liveness::
953    /// compute_liveness`'s declared-local discovery needs to walk the
954    /// *pre-flattening* statement tree looking for `Block` statements'
955    /// `block_symbol()` scopes -- `blocks`/`stmts` above only ever retain
956    /// the leaf statements `CfgBuilder::walk_seq` flattened into each
957    /// `BasicBlock`, which discards every intervening `begin`/`end` `Block`
958    /// statement node (and, with it, that block's `StatementBlockSymbol` --
959    /// the ONLY place `sv-lang`=0.1.9's pinned C API exposes a declared
960    /// local's `Symbol`; there is no per-`VariableDeclaration`-statement
961    /// symbol accessor at all, confirmed against the real binding). Keeping
962    /// the untouched root here, alongside the flattened `blocks`, is the
963    /// minimal way to make both queries possible without re-walking from
964    /// the caller.
965    pub body: Statement<'d>,
966    /// The formals and locals of every task inlined into this CFG
967    /// ([`ArgBinding`]) -- declared locals of the process for frame-layout
968    /// purposes, though no `Block` under `body` declares them.
969    pub inlined_locals: Vec<Symbol<'d>>,
970    /// The target of each intra-assignment-timed assignment's captured
971    /// right-hand side ([`ArgBinding::Capture`]), indexed by `temp`: its
972    /// type sizes the temporary's frame slot.
973    pub captures: Vec<Expression<'d>>,
974    /// For a fork branch: the variables of the forking activation it uses
975    /// (LRM 1800-2023 §9.3.2, §6.21) -- shared with it, not copied: their
976    /// frame slots, here and in the parent, hold a pointer to one cell.
977    pub captured: Vec<Symbol<'d>>,
978    /// `(formal, actual)`: a `ref` formal of an inlined task that every call
979    /// in this process binds to the same static variable or net (LRM
980    /// 1800-2023 §13.5.2). The formal names the actual's storage -- it has no
981    /// frame slot and no copy-in/copy-out -- so a wait on it (`@(posedge
982    /// clk)` with `ref logic clk`) sees the actual change.
983    pub ref_bindings: Vec<(Symbol<'d>, Symbol<'d>)>,
984    /// The name(s) a `disable <label>;` ANYWHERE (this process, a sibling
985    /// branch, an ancestor fork, or a genuinely different process) may
986    /// single this CFG's own live process out by (LRM 9.6.2): the
987    /// enclosing `fork : NAME`'s name, if this CFG is one of its branches,
988    /// and/or this CFG's own named outermost block (`peer_block(body)`),
989    /// if it has one. Empty when nothing could ever name this process this
990    /// way. `sv-sim-codegen::process_lower`'s fork/join lowering copies this
991    /// into each branch's own [`super::super::sched::SpawnSpec::
992    /// disable_name`].
993    pub disable_names: Vec<String>,
994    /// Every name [`Self::disable_names`] of a fork branch this CFG's own
995    /// walk resolved a `disable <label>;` against WITHOUT a same-process
996    /// lexical match (not an enclosing [`BasicBlock`], not this CFG's own
997    /// `fork_name`, not a procedural-assertion label): the kept `disable`
998    /// statement's codegen (`sv-sim-codegen::process_lower`) reads this to
999    /// tell such a statement apart from the ones that resolve to the OLDER,
1000    /// same-fork-only `sv_rt_disable_fork_block` path -- see
1001    /// [`CfgBuilder::fork_branch_names`]'s own doc comment for how it
1002    /// accumulates (parent-to-branch, branch-to-branch, and forward past
1003    /// the fork statement that spawned them).
1004    pub fork_branch_names: Vec<String>,
1005    /// `(resume, label, exit)`: while the process is suspended and will resume
1006    /// at block `resume`, it is inside the named block or task activation
1007    /// `label`, which a `disable label;` of another process ends by resuming
1008    /// the process at block `exit` instead (LRM 9.6.2). Only labels some
1009    /// `disable` of the design names ([`is_disabled_somewhere`]) are listed.
1010    /// A segment index is a valid resume point of the compiled kernel.
1011    pub disable_scopes: Vec<(BlockId, String, BlockId)>,
1012    /// The labels of the `disable` statements this CFG keeps as statements
1013    /// because they must also reach the processes other than this one that
1014    /// are inside the named block or task (`sv_rt_disable_named`), with
1015    /// whether the statement is the whole effect of the disable (an
1016    /// unresolved target: no jump follows, so codegen emits the VPI
1017    /// `cbDisable` probe itself).
1018    pub disable_broadcast: Vec<(String, bool)>,
1019    /// For a synthetic fork branch ([`CfgBuilder::walk_nba_event`]): the
1020    /// frame slots of the forking process copied BY VALUE into this
1021    /// branch's own at the spawn (LRM 1800-2023 §10.4.2: the right-hand
1022    /// side and the repeat count are evaluated when the assignment is
1023    /// reached, not when its event fires).
1024    pub inherited: Vec<InheritedSlot>,
1025    /// Per capture temporary (`captures`), the formal whose type sizes its
1026    /// frame slot instead of the captured expression's (a recursive task's
1027    /// call arguments, [`CfgBuilder::inline_recursive_task`]); entries past
1028    /// the end are `None`.
1029    pub capture_types: Vec<Option<Symbol<'d>>>,
1030    /// Whether this CFG is a recursive task's activation ([`Recursion`]).
1031    pub recursion: Recursion,
1032    /// Two fork branches carrying the same id were built from the same
1033    /// statement in the same inherited context ([`ForkBranchKey`]), so
1034    /// they are the same CFG and lower to the same kernel: the lowering
1035    /// compiles it once per process and points every spawn at that code
1036    /// (a UVM testbench otherwise cloned each branch once per path of
1037    /// nested forks leading to it, tens of GB of IR). `None` for a branch
1038    /// that is not shared.
1039    pub share_id: Option<u64>,
1040}
1041
1042/// A fork branch's part in a recursive task with timing controls (LRM
1043/// 1800-2023 §13.4, §13.4.1): every call is a `join` of one forked child
1044/// running the task body in a frame of its own.
1045#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1046pub enum Recursion {
1047    /// An ordinary CFG.
1048    None,
1049    /// The task's body: the branch the first call forks. Its kernel is
1050    /// compiled once and also serves every recursive call inside it.
1051    Root,
1052    /// A placeholder branch (no blocks of its own): a recursive call inside
1053    /// the enclosing [`Recursion::Root`], which spawns that root's kernel again.
1054    SelfSpawn,
1055    /// A placeholder branch: a call, inside one recursive activation, of the
1056    /// task of an enclosing activation (mutual recursion: `a` calls `b` calls
1057    /// `a`). It spawns that activation's kernel again; the number says how
1058    /// many [`Recursion::Root`] levels out it is (1 = the activation this one
1059    /// was forked from).
1060    AncestorSpawn(u32),
1061}
1062
1063/// Whether the process re-triggers from `entry` after finishing one pass
1064/// through its body (`always`/`always_ff`/`always_comb`/`always_latch`) or
1065/// runs exactly once (`initial`/`final`) -- PR-048's `Terminator::End{
1066/// loop_back}` distinction. Deliberately **not**
1067/// `sv-sim-codegen::module::SliceProcKind`: this crate must never depend on
1068/// `sv-sim-codegen` (see [`LoweringError`]'s doc comment for the same
1069/// reasoning); a caller that already has a `SliceProcKind` (module
1070/// discovery, Task S2/S7) maps it down to this coarser 2-value distinction
1071/// itself (`Initial` -> `OneShot`, `Always`/`AlwaysFF` -> `Looping`).
1072#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1073pub enum ProcessLoopKind {
1074    OneShot,
1075    Looping,
1076}
1077
1078/// PR-007: the single recursive-descent CFG builder entry point.
1079pub fn build_cfg<'d>(
1080    body: &Statement<'d>,
1081    loop_kind: ProcessLoopKind,
1082) -> Result<ProcessCfg<'d>, LoweringError> {
1083    build_cfg_in(
1084        body,
1085        loop_kind,
1086        None,
1087        None,
1088        None,
1089        None,
1090        Inherited::default(),
1091        None,
1092    )
1093}
1094
1095/// The CFG of a one-shot process that runs the body of `task` once, for a
1096/// task exported to C (`export "DPI-C" task`, LRM 1800-2023 35.7) whose body
1097/// consumes time (or stages a nonblocking assignment): C cannot run it as a
1098/// function, so each call from C spawns a process of this CFG and the
1099/// importing task blocks until it finishes (35.5.3).
1100///
1101/// The formals are ordinary locals of the process's frame, with NO entry
1102/// binding: the spawner stores the C arguments into their frame slots before
1103/// the process starts and reads the `output`/`inout` ones back when it ends
1104/// (the process's frame is what a copy-in/copy-out call has in a caller).
1105/// Other automatic locals get their declaration initialisers as usual.
1106/// `static` formals are module state, not frame slots; they stay out of
1107/// `inlined_locals`, exactly as a call inlined into a process treats them.
1108///
1109/// Fails by name for a task that calls itself (a recursive activation needs
1110/// the fork-per-call machinery of [`Recursion::Root`], whose argument
1111/// capture an export cannot supply).
1112pub fn build_export_task_cfg<'d>(
1113    task: &Symbol<'d>,
1114    global_clock: Option<InferredClockEvent<'d>>,
1115) -> Result<ProcessCfg<'d>, LoweringError> {
1116    let body = proc_body(task).ok_or_else(|| {
1117        LoweringError::malformed(format!("exported task `{}` has no body", task.name()))
1118    })?;
1119    let mut b = CfgBuilder::new();
1120    b.global_clock = global_clock;
1121    let entry = b.alloc_block();
1122    // Calling itself from its own body would inline without end; the
1123    // recursion machinery needs a caller's actuals to type its captures.
1124    b.rec_outer.push(task.uid());
1125    for f in task.subroutine_arguments() {
1126        if !crate::netlist::is_static_procedural_var(&f) {
1127            b.inlined_locals.push(f);
1128        }
1129    }
1130    for local in task_locals(task) {
1131        if crate::netlist::is_static_procedural_var(&local) {
1132            continue;
1133        }
1134        if !is_automatic_block_var(&local) {
1135            b.blocks[entry.0 as usize]
1136                .bindings
1137                .push(ArgBinding::Init { var: local });
1138        }
1139        b.inlined_locals.push(local);
1140    }
1141    for v in crate::patterns::pattern_vars(body.as_sem_node()) {
1142        if !b.inlined_locals.iter().any(|l| l.uid() == v.uid()) {
1143            b.inlined_locals.push(v);
1144        }
1145    }
1146    let after = b.alloc_block();
1147    b.target_depth.insert(after, 0);
1148    b.return_targets.push(after);
1149    let exit = b.walk_stmt(body, entry)?;
1150    b.close(exit, Terminator::Fallthrough(after));
1151    b.close(after, Terminator::End { loop_back: None });
1152    let inlined_locals = std::mem::take(&mut b.inlined_locals);
1153    let captures = std::mem::take(&mut b.captures);
1154    let capture_types = std::mem::take(&mut b.capture_types);
1155    let ref_bindings = b.take_ref_bindings();
1156    let disable_scopes = b.take_disable_scopes();
1157    let disable_broadcast = std::mem::take(&mut b.disable_broadcast);
1158    let fork_branch_names = std::mem::take(&mut b.fork_branch_names);
1159    Ok(ProcessCfg {
1160        blocks: b.finish(),
1161        entry,
1162        body,
1163        inlined_locals,
1164        captures,
1165        captured: Vec::new(),
1166        ref_bindings,
1167        disable_names: Vec::new(),
1168        fork_branch_names,
1169        disable_scopes,
1170        disable_broadcast,
1171        inherited: Vec::new(),
1172        capture_types,
1173        recursion: Recursion::None,
1174        share_id: None,
1175    })
1176}
1177
1178/// The one-shot process a task called from a sequence match item runs as
1179/// (LRM 1800-2023 16.11: "the subroutine call is scheduled in the Reactive
1180/// region ... as a thread of its own"). Its body is the call `call` (a user
1181/// task that can suspend, [`subroutine_can_suspend`]) inlined the way a call
1182/// statement of an ordinary process is: the formals are bound from the actual
1183/// expressions by [`ArgBinding::In`] in the entry block, so the process reads
1184/// its by-value actuals when it starts (the assertion kernel lowers each as the
1185/// value the engine sampled at the end point, `sva::proc::call_arg_key`) and
1186/// every call is its own activation with its own frame.
1187///
1188/// Fails by name for a task that calls itself (a recursive activation needs the
1189/// fork-per-call machinery of [`Recursion::Root`], whose argument capture a
1190/// spawned call cannot supply).
1191pub fn build_match_item_task_cfg<'d>(
1192    call: Expression<'d>,
1193) -> Result<ProcessCfg<'d>, LoweringError> {
1194    let task = call.call_subroutine().ok_or_else(|| {
1195        LoweringError::malformed("a match-item call of a task has no subroutine symbol")
1196    })?;
1197    let body = proc_body(&task)
1198        .ok_or_else(|| LoweringError::malformed(format!("task `{}` has no body", task.name())))?;
1199    let mut b = CfgBuilder::new();
1200    let entry = b.alloc_block();
1201    let exit = b.inline_task_call(call, task, entry)?;
1202    b.close(exit, Terminator::End { loop_back: None });
1203    let inlined_locals = std::mem::take(&mut b.inlined_locals);
1204    let captures = std::mem::take(&mut b.captures);
1205    let capture_types = std::mem::take(&mut b.capture_types);
1206    let ref_bindings = b.take_ref_bindings();
1207    let disable_scopes = b.take_disable_scopes();
1208    let disable_broadcast = std::mem::take(&mut b.disable_broadcast);
1209    let fork_branch_names = std::mem::take(&mut b.fork_branch_names);
1210    Ok(ProcessCfg {
1211        blocks: b.finish(),
1212        entry,
1213        body,
1214        inlined_locals,
1215        captures,
1216        captured: Vec::new(),
1217        ref_bindings,
1218        disable_names: Vec::new(),
1219        fork_branch_names,
1220        disable_scopes,
1221        disable_broadcast,
1222        inherited: Vec::new(),
1223        capture_types,
1224        recursion: Recursion::None,
1225        share_id: None,
1226    })
1227}
1228
1229/// The clocking event a checker's `$inferred_clock` (LRM 17.4) stands for: the
1230/// signal and edge of the default clocking of the scope that instantiates the
1231/// checker (14.12), which a checker procedure's `@clock` waits on when `clock`
1232/// kept its default. The third member is the event's `iff` condition
1233/// (`default clocking @(posedge clk iff en);`, 9.4.2.3), which gates every wait
1234/// on it; `None` for `@$global_clock`.
1235pub type InferredClockEvent<'d> = (Expression<'d>, EdgeKind, Option<Expression<'d>>);
1236
1237/// Whether `signal` (the expression of a signal event control) is a call of
1238/// `$global_clock` (14.14).
1239pub fn is_global_clock_call(signal: &Expression<'_>) -> bool {
1240    signal.kind() == ExpressionKind::Call
1241        && signal.is_system_call() == Some(true)
1242        && signal
1243            .system_subroutine()
1244            .is_some_and(|s| s.known_name_id() == sv_lang::KnownSystemName::GlobalClock)
1245}
1246
1247/// Whether `signal` (the expression of a signal event control) is a call of
1248/// `$inferred_clock`: a checker's `event clock = $inferred_clock` formal, used as
1249/// `@clock` by a procedure of an instance that did not bind it (17.4).
1250pub fn is_inferred_clock_call(signal: &Expression<'_>) -> bool {
1251    signal.kind() == ExpressionKind::Call
1252        && signal.is_system_call() == Some(true)
1253        && signal
1254            .system_subroutine()
1255            .is_some_and(|s| s.known_name_id() == sv_lang::KnownSystemName::InferredClock)
1256}
1257
1258/// The clocking block of a drive target rooted in a clock variable
1259/// (`cb.q`, `cb.q[3:0]`; LRM 1800-2023 14.16), else `None`.
1260fn drive_clocking_of<'d>(mut target: Expression<'d>) -> Option<Symbol<'d>> {
1261    while matches!(
1262        target.kind(),
1263        ExpressionKind::ElementSelect | ExpressionKind::RangeSelect
1264    ) {
1265        target = target.select_value()?;
1266    }
1267    let var = target.referenced_symbol()?;
1268    if var.kind() != SymbolKind::ClockVar {
1269        return None;
1270    }
1271    var.parent()
1272        .filter(|p| p.kind() == SymbolKind::ClockingBlock)
1273}
1274
1275/// The clock variable of a drive target rooted in a virtual interface member
1276/// access (`v.sb.c`, `v.sb.c[3:0]`), else `None`.
1277fn drive_via_of<'d>(mut target: Expression<'d>) -> Option<Expression<'d>> {
1278    while matches!(
1279        target.kind(),
1280        ExpressionKind::ElementSelect | ExpressionKind::RangeSelect
1281    ) {
1282        target = target.select_value()?;
1283    }
1284    (crate::netlist::vif::is_member_access(&target)).then_some(target)
1285}
1286
1287/// [`build_cfg`] for a process of an instance whose default clocking block
1288/// is `default_clocking` (what `##N` counts).
1289pub fn build_cfg_clocked<'d>(
1290    body: &Statement<'d>,
1291    loop_kind: ProcessLoopKind,
1292    default_clocking: Option<Symbol<'d>>,
1293) -> Result<ProcessCfg<'d>, LoweringError> {
1294    build_cfg_scoped(body, loop_kind, default_clocking, &[], None)
1295}
1296
1297/// [`build_cfg_clocked`] for a process that shares its instance with the
1298/// processes whose outermost named blocks are `peers`: a `disable` of one of
1299/// them is accepted when it cannot be inside the block (see [`PeerBlock`]).
1300/// `inferred_clock` is the event a checker procedure's `@clock` waits on when its
1301/// `clock` formal kept the default `$inferred_clock` (17.4); `None` for a process
1302/// that is not a checker's or whose instantiation context infers no clock.
1303pub fn build_cfg_scoped<'d>(
1304    body: &Statement<'d>,
1305    loop_kind: ProcessLoopKind,
1306    default_clocking: Option<Symbol<'d>>,
1307    peers: &[PeerBlock],
1308    inferred_clock: Option<InferredClockEvent<'d>>,
1309) -> Result<ProcessCfg<'d>, LoweringError> {
1310    build_cfg_in(
1311        body,
1312        loop_kind,
1313        None,
1314        default_clocking,
1315        Some(peers),
1316        inferred_clock,
1317        Inherited::default(),
1318        None,
1319    )
1320}
1321
1322/// [`build_cfg_scoped`] that can dispatch a virtual class task that suspends
1323/// over the class templates of `virtual_tasks`.
1324pub fn build_cfg_scoped_with_virtuals<'d>(
1325    body: &Statement<'d>,
1326    loop_kind: ProcessLoopKind,
1327    default_clocking: Option<Symbol<'d>>,
1328    peers: &[PeerBlock],
1329    inferred_clock: Option<InferredClockEvent<'d>>,
1330    global_clock: Option<InferredClockEvent<'d>>,
1331    virtual_tasks: Option<std::rc::Rc<VirtualTasks<'d>>>,
1332) -> Result<ProcessCfg<'d>, LoweringError> {
1333    build_cfg_in(
1334        body,
1335        loop_kind,
1336        None,
1337        default_clocking,
1338        Some(peers),
1339        inferred_clock,
1340        Inherited {
1341            global_clock,
1342            ..Inherited::default()
1343        },
1344        virtual_tasks,
1345    )
1346}
1347
1348/// The outermost named block of another process of the instance, which a
1349/// `disable` may target (LRM 9.6.2, 16.14.6.4).
1350#[derive(Clone, Debug, PartialEq, Eq)]
1351pub struct PeerBlock {
1352    pub name: String,
1353    /// The block contains a suspend point, so its process may be inside it. A
1354    /// `disable` then has to terminate the process's activity (9.6.2), which
1355    /// the scheduler cannot do; when the block cannot suspend, the process is
1356    /// never inside it while another runs, and the disable's only effect is the
1357    /// flush of the process's pending procedural assertion queue.
1358    pub can_suspend: bool,
1359    /// The block is the outermost block of a fork branch (its own process,
1360    /// tagged by name): any number of those may share a name, and `disable
1361    /// <name>;` ends every one (LRM 9.6.2).
1362    pub fork_branch: bool,
1363}
1364
1365/// The outermost scope of a process body (LRM 16.14.6.2): the block statement
1366/// the body is, looking through the leading timing controls of an `always`
1367/// (`always @(a or b) begin : b2 ... end`). `None` when the body is not a block.
1368pub fn outermost_block<'d>(body: &Statement<'d>) -> Option<Statement<'d>> {
1369    let mut cur = *body;
1370    loop {
1371        // Not a `match` over the kind: every other statement kind means "the body
1372        // is not a block", which is an answer, not an unimplemented case.
1373        let kind = cur.kind();
1374        if kind == StatementKind::Block {
1375            return Some(cur);
1376        }
1377        if kind == StatementKind::Timed {
1378            cur = cur.body()?;
1379        } else if kind == StatementKind::List {
1380            let list = live_statements(&cur);
1381            let [only] = list.as_slice() else {
1382                return None;
1383            };
1384            cur = *only;
1385        } else {
1386            return None;
1387        }
1388    }
1389}
1390
1391/// The named outermost block of the process body `body`, as another process
1392/// sees it ([`PeerBlock`]); `None` for an unnamed one.
1393pub fn peer_block<'d>(body: &Statement<'d>) -> Option<PeerBlock> {
1394    let block = outermost_block(body)?;
1395    let name = block_name(&block);
1396    if name.is_empty() {
1397        return None;
1398    }
1399    Some(PeerBlock {
1400        name,
1401        can_suspend: contains_suspend_trigger_in(
1402            block.as_sem_node(),
1403            &mut std::collections::HashSet::new(),
1404            true,
1405            false,
1406        ),
1407        fork_branch: false,
1408    })
1409}
1410
1411/// The hierarchical path of every named block (sequential or fork) in the
1412/// process body `body`, dotted from its outermost named block down
1413/// (`a`, `a.b`, `a.b.c`, `a.f.br1`): what a `disable s.a.b.c.inner;` from
1414/// another instance names, however deep (LRM 9.6.2, 23.6).
1415pub fn named_block_paths(body: &Statement<'_>) -> Vec<String> {
1416    let mut out = Vec::new();
1417    let mut stack: Vec<(SemNode<'_>, String)> = vec![(body.as_sem_node(), String::new())];
1418    while let Some((n, prefix)) = stack.pop() {
1419        let mut here = prefix.clone();
1420        if let Some(s) = n.as_statement()
1421            && s.kind() == StatementKind::Block
1422        {
1423            let name = block_name(&s);
1424            if !name.is_empty() {
1425                here = if prefix.is_empty() {
1426                    name
1427                } else {
1428                    format!("{prefix}.{name}")
1429                };
1430                out.push(here.clone());
1431            }
1432        }
1433        stack.extend(live_children(&n).into_iter().map(|c| (c, here.clone())));
1434    }
1435    out
1436}
1437
1438/// The named sequential blocks nested in the process body `body` below its
1439/// outermost scope, as another process sees them ([`PeerBlock`]): a `disable`
1440/// of one of them from another process ends the activity inside it (LRM
1441/// 9.6.2). A fork's branches are processes of their own and are not entered.
1442pub fn nested_peer_blocks<'d>(body: &Statement<'d>) -> Vec<PeerBlock> {
1443    let outermost = outermost_block(body).map(|b| b.source_range().start.offset);
1444    let mut out = Vec::new();
1445    let mut stack = vec![body.as_sem_node()];
1446    while let Some(n) = stack.pop() {
1447        if let Some(s) = n.as_statement()
1448            && s.kind() == StatementKind::Block
1449        {
1450            match s.block_kind() {
1451                Some(StatementBlockKind::Sequential) => {
1452                    let name = block_name(&s);
1453                    if !name.is_empty() && outermost != Some(s.source_range().start.offset) {
1454                        out.push(PeerBlock {
1455                            name,
1456                            can_suspend: contains_suspend_trigger_in(
1457                                n,
1458                                &mut std::collections::HashSet::new(),
1459                                true,
1460                                false,
1461                            ),
1462                            fork_branch: false,
1463                        });
1464                    }
1465                }
1466                Some(
1467                    StatementBlockKind::JoinAll
1468                    | StatementBlockKind::JoinAny
1469                    | StatementBlockKind::JoinNone,
1470                ) => {
1471                    fork_branch_peers(n, &mut out);
1472                    continue;
1473                }
1474                None => {}
1475            }
1476        }
1477        stack.extend(live_children(&n));
1478    }
1479    out
1480}
1481
1482/// The named outermost blocks of the branches of the fork `fork` and of every
1483/// fork nested in those branches: each branch is a process of its own tagged
1484/// with its block's name ([`ProcessCfg::disable_names`]), which `disable
1485/// <name>;` of another process ends (LRM 9.6.2). A branch's process may be
1486/// anywhere, so these always `can_suspend`. Named blocks nested deeper in a
1487/// branch are listed as ordinary nested peers.
1488fn fork_branch_peers(fork: SemNode<'_>, out: &mut Vec<PeerBlock>) {
1489    let mut branches = fork.children();
1490    if let [only] = branches.as_slice()
1491        && only
1492            .as_statement()
1493            .is_some_and(|s| s.kind() == StatementKind::List)
1494    {
1495        branches = only.children();
1496    }
1497    for b in branches {
1498        let Some(s) = b.as_statement() else { continue };
1499        if let Some(mut p) = peer_block(&s) {
1500            p.can_suspend = true;
1501            p.fork_branch = true;
1502            out.push(p);
1503        }
1504        // Blocks nested in the branch (and in forks nested in it) are blocks
1505        // of the branch's process: its kernel carries their disable scopes
1506        // (`ProcessCfg::disable_scopes`, `ForkKernel::disable_scopes`).
1507        out.extend(nested_peer_blocks(&s));
1508    }
1509}
1510
1511impl ProcessCfg<'_> {
1512    /// The labels of the disable scopes of every fork branch under this CFG
1513    /// (nested forks included): the blocks of the branch kernels a
1514    /// `disable` of another process can end (LRM 9.6.2).
1515    pub fn branch_disable_labels(&self) -> Vec<String> {
1516        let mut out = Vec::new();
1517        for b in &self.blocks {
1518            if let Terminator::ForkJoin { branches, .. } = &b.terminator {
1519                for br in branches {
1520                    out.extend(br.disable_scopes.iter().map(|(_, l, _)| l.clone()));
1521                    out.extend(br.branch_disable_labels());
1522                }
1523            }
1524        }
1525        out
1526    }
1527
1528    /// The `disable` broadcast targets of every fork branch under this CFG
1529    /// (nested forks included), as [`ProcessCfg::disable_broadcast`]: a
1530    /// `disable u1.blk;` inside a branch must resolve as one in the process
1531    /// itself does, or be refused (LRM 9.6.2).
1532    pub fn branch_disable_broadcast(&self) -> Vec<(String, bool)> {
1533        let mut out = Vec::new();
1534        for b in &self.blocks {
1535            if let Terminator::ForkJoin { branches, .. } = &b.terminator {
1536                for br in branches {
1537                    out.extend(br.disable_broadcast.iter().cloned());
1538                    out.extend(br.branch_disable_broadcast());
1539                }
1540            }
1541        }
1542        out
1543    }
1544}
1545
1546/// The label of every procedural concurrent assertion (not an `expect`, which
1547/// blocks) in the subtree of `body`, with the assertion statement: a labeled
1548/// statement is a block of that name holding it, which `disable <label>;`
1549/// names (16.14.6.4).
1550pub fn assertion_labels<'d>(body: &Statement<'d>) -> Vec<(String, Statement<'d>)> {
1551    let mut out = Vec::new();
1552    let mut stack = vec![body.as_sem_node()];
1553    while let Some(n) = stack.pop() {
1554        if let Some(s) = n.as_statement()
1555            && s.kind() == StatementKind::Block
1556            && let name = block_name(&s)
1557            && !name.is_empty()
1558            && let [only] = s.statements().as_slice()
1559            && only.kind() == StatementKind::ConcurrentAssertion
1560            && !is_expect(only)
1561        {
1562            out.push((name, *only));
1563        }
1564        stack.extend(live_children(&n));
1565    }
1566    out
1567}
1568
1569/// The label of every deferred immediate assertion (`L: assert #0 (c) ...;`,
1570/// `assert final`) in the subtree of `body`, with the assertion statement: a
1571/// labeled statement is a block of that name holding it, which `disable L;`
1572/// names (LRM 16.4.4: cancels that assertion's pending reports).
1573pub fn deferred_assertion_labels<'d>(body: &Statement<'d>) -> Vec<(String, Statement<'d>)> {
1574    let mut out = Vec::new();
1575    let mut stack = vec![body.as_sem_node()];
1576    while let Some(n) = stack.pop() {
1577        if let Some(s) = n.as_statement()
1578            && s.kind() == StatementKind::Block
1579            && let name = block_name(&s)
1580            && !name.is_empty()
1581            && let [only] = s.statements().as_slice()
1582            && only.kind() == StatementKind::ImmediateAssertion
1583            && only.is_deferred_assertion() == Some(true)
1584        {
1585            out.push((name, *only));
1586        }
1587        stack.extend(live_children(&n));
1588    }
1589    out
1590}
1591
1592/// The label of every plain (non-deferred) immediate assertion in the subtree
1593/// of `body`: a labeled statement is a block of that name holding it. A
1594/// `disable L;` outside that block (LRM 9.6.2) finds the block already
1595/// finished -- an immediate assertion's own block has no suspend point the
1596/// disable could catch from the same process.
1597pub fn immediate_assertion_labels(body: &Statement<'_>) -> Vec<String> {
1598    let mut out = Vec::new();
1599    let mut stack = vec![body.as_sem_node()];
1600    while let Some(n) = stack.pop() {
1601        if let Some(s) = n.as_statement()
1602            && s.kind() == StatementKind::Block
1603            && let name = block_name(&s)
1604            && !name.is_empty()
1605            && let [only] = s.statements().as_slice()
1606            && only.kind() == StatementKind::ImmediateAssertion
1607            && only.is_deferred_assertion() != Some(true)
1608        {
1609            out.push(name);
1610        }
1611        stack.extend(live_children(&n));
1612    }
1613    out
1614}
1615
1616/// Whether `stmt` is a labeled immediate assertion (`L: assert (c) begin
1617/// ... disable L; ... end`, a block named `L` holding only the assertion)
1618/// whose only `disable` targets `L` itself and which contains no other
1619/// suspend point. Disabling the label ends the assertion statement (LRM
1620/// 9.6.2: a labeled statement is a named scope), so the whole block is one
1621/// straight-line leaf for the CFG and codegen leaves it through an exit
1622/// block (`process_lower`'s `List | Block` arm).
1623pub fn self_disabling_assertion_block(stmt: &Statement<'_>) -> bool {
1624    if stmt.kind() != StatementKind::Block {
1625        return false;
1626    }
1627    let name = block_name(stmt);
1628    if name.is_empty() {
1629        return false;
1630    }
1631    let children = stmt.statements();
1632    let [only] = children.as_slice() else {
1633        return false;
1634    };
1635    if only.kind() != StatementKind::ImmediateAssertion {
1636        return false;
1637    }
1638    let mut disables = 0usize;
1639    let mut stack = vec![only.as_sem_node()];
1640    while let Some(n) = stack.pop() {
1641        if let Some(s) = n.as_statement()
1642            && s.kind() == StatementKind::Disable
1643        {
1644            match disable_label(&s) {
1645                Ok(l) if l == name => disables += 1,
1646                _ => return false,
1647            }
1648        }
1649        stack.extend(live_children(&n));
1650    }
1651    disables > 0
1652        && !contains_suspend_trigger_in(
1653            only.as_sem_node(),
1654            &mut std::collections::HashSet::new(),
1655            true,
1656            false,
1657        )
1658}
1659
1660/// The CFG of the plan that hosts the `expect` statement `stmt` for the
1661/// assertion engine (LRM 1800-2023 §16.17): `stmt` is an ordinary leaf of it,
1662/// not a wait -- the process that blocks on the `expect` is another plan, built
1663/// by [`build_cfg_clocked`].
1664pub fn build_cfg_hosted_assertion<'d>(
1665    stmt: &Statement<'d>,
1666    default_clocking: Option<Symbol<'d>>,
1667) -> Result<ProcessCfg<'d>, LoweringError> {
1668    let mut builder = CfgBuilder::new();
1669    builder.default_clocking = default_clocking;
1670    builder.expect_as_leaf = true;
1671    let entry = builder.alloc_block();
1672    let exit = builder.walk_stmt(*stmt, entry)?;
1673    builder.close(exit, Terminator::End { loop_back: None });
1674    Ok(ProcessCfg {
1675        blocks: builder.finish(),
1676        entry,
1677        body: *stmt,
1678        inlined_locals: Vec::new(),
1679        captures: Vec::new(),
1680        captured: Vec::new(),
1681        ref_bindings: Vec::new(),
1682        disable_names: Vec::new(),
1683        fork_branch_names: Vec::new(),
1684        disable_scopes: Vec::new(),
1685        disable_broadcast: Vec::new(),
1686        inherited: Vec::new(),
1687        capture_types: Vec::new(),
1688        recursion: Recursion::None,
1689        share_id: None,
1690    })
1691}
1692
1693/// Whether a call was written `super.m(...)` (LRM 8.15: not virtual).
1694pub type SuperDetector = fn(Expression<'_>) -> bool;
1695
1696thread_local! {
1697    /// The [`SuperDetector`]: recovering `super.` needs the design's syntax,
1698    /// which codegen holds (`sv_sim_codegen::classes::call_is_explicit_super`);
1699    /// the run installs it.
1700    static EXPLICIT_SUPER: std::cell::Cell<Option<SuperDetector>> =
1701        const { std::cell::Cell::new(None) };
1702}
1703
1704/// Installs (or, with `None`, removes) the `super.`-call detector
1705/// [`CfgBuilder::inline_task_call`] uses for virtual class tasks.
1706pub fn set_explicit_super_detector(f: Option<SuperDetector>) {
1707    EXPLICIT_SUPER.with(|c| c.set(f));
1708}
1709
1710fn is_explicit_super(call: Expression<'_>) -> bool {
1711    EXPLICIT_SUPER.with(|c| c.get().is_some_and(|f| f(call)))
1712}
1713
1714/// The design's class templates a dynamic type can be (every `new()`-reachable
1715/// class and its ancestors, with their dense template ids), for dispatching a
1716/// virtual class task that can suspend: such a task is inlined, so a call
1717/// through a base handle becomes a switch over the overrides the possible
1718/// dynamic classes resolve it to.
1719pub struct VirtualTasks<'d> {
1720    pub classes: Vec<(u32, sv_lang::Type<'d>)>,
1721    /// Per entry of `classes`, its base chain (root first, itself last) and
1722    /// the chain's symbol keys, filled on first use: `implementations` runs
1723    /// once per virtual method per fixed-point round, and rebuilding the
1724    /// chain and every ancestor's hierarchical path each time dominated the
1725    /// compile of a deep class library (`import uvm_pkg::*`).
1726    chains: std::cell::OnceCell<Vec<ClassChain<'d>>>,
1727    /// Per task, whether its body reaches itself ([`task_reaches`] with the
1728    /// task as target): decided once per task, not per call site, because
1729    /// each query walks every task reachable through virtual dispatch (a
1730    /// UVM testbench compiled for over an hour re-walking `uvm_pkg`).
1731    self_reach: std::cell::RefCell<std::collections::HashMap<crate::symid::SymbolId, bool>>,
1732}
1733
1734/// One class's base chain and the keys of its members ([`VirtualTasks`]).
1735struct ClassChain<'d> {
1736    chain: Vec<sv_lang::Type<'d>>,
1737    keys: std::collections::HashSet<crate::symid::SymbolId>,
1738}
1739
1740thread_local! {
1741    /// Every virtual method one of whose implementations (in the classes a
1742    /// dynamic type can be) can suspend ([`set_suspending_virtuals`]).
1743    static SUSPENDING_VIRTUALS: std::cell::RefCell<std::collections::HashSet<crate::symid::SymbolId>> =
1744        std::cell::RefCell::new(std::collections::HashSet::new());
1745}
1746
1747fn is_suspending_virtual(sub: &Symbol<'_>) -> bool {
1748    SUSPENDING_VIRTUALS.with(|s| s.borrow().contains(&sub.uid()))
1749}
1750
1751/// Records which virtual methods can suspend through an override (LRM 8.20:
1752/// a call runs the dynamic class's implementation, so it suspends when any
1753/// implementation can): such a call is inlined as a dispatch over the
1754/// overrides. To a fixed point, since an implementation may suspend only
1755/// through another virtual call. Clears the previous design's record.
1756pub fn set_suspending_virtuals(vt: &VirtualTasks<'_>) {
1757    SUSPENDING_VIRTUALS.with(|s| s.borrow_mut().clear());
1758    let mut methods: Vec<(crate::symid::SymbolId, Symbol<'_>)> = Vec::new();
1759    for (_, ty) in &vt.classes {
1760        for m in ty.as_symbol().members() {
1761            let m = if m.kind() == SymbolKind::MethodPrototype {
1762                match m.method_prototype_subroutine() {
1763                    Some(real) => real,
1764                    None => continue,
1765                }
1766            } else {
1767                m
1768            };
1769            if m.kind() == SymbolKind::Subroutine
1770                && (m.subroutine_this_var().is_some()
1771                    || m.subroutine_flags().contains(sv_lang::MethodFlags::PURE))
1772                && (m.subroutine_flags().contains(sv_lang::MethodFlags::VIRTUAL)
1773                    || m.subroutine_override().is_some())
1774                && !methods.iter().any(|(_, x)| *x == m)
1775            {
1776                methods.push((m.uid(), m));
1777            }
1778        }
1779    }
1780    loop {
1781        let mut changed = false;
1782        for (uid, m) in &methods {
1783            if SUSPENDING_VIRTUALS.with(|s| s.borrow().contains(uid)) {
1784                continue;
1785            }
1786            if vt
1787                .implementations(m)
1788                .iter()
1789                .any(|(_, imp)| subroutine_can_suspend(imp))
1790            {
1791                SUSPENDING_VIRTUALS.with(|s| s.borrow_mut().insert(uid.clone()));
1792                changed = true;
1793            }
1794        }
1795        if !changed {
1796            break;
1797        }
1798    }
1799}
1800
1801impl<'d> VirtualTasks<'d> {
1802    /// The possible dynamic classes (template id, class type).
1803    pub fn new(classes: Vec<(u32, sv_lang::Type<'d>)>) -> Self {
1804        VirtualTasks {
1805            classes,
1806            chains: std::cell::OnceCell::new(),
1807            self_reach: std::cell::RefCell::new(std::collections::HashMap::new()),
1808        }
1809    }
1810
1811    /// The implementations of `method` the dynamic classes derived from (or
1812    /// equal to) its declaring class run, each with the template ids that
1813    /// run it: per class, the most-derived declaration of the name with a
1814    /// body.
1815    pub fn implementations(&self, method: &Symbol<'d>) -> Vec<(Vec<u32>, Symbol<'d>)> {
1816        let Some(decl) = method.parent() else {
1817            return Vec::new();
1818        };
1819        let name = method.name();
1820        let decl_key = decl.uid();
1821        let chains = self.chains.get_or_init(|| {
1822            self.classes
1823                .iter()
1824                .map(|(_, ty)| {
1825                    let mut chain = crate::netlist::base_chain(*ty);
1826                    chain.push(*ty);
1827                    let keys = chain.iter().map(|c| c.as_symbol().uid()).collect();
1828                    ClassChain { chain, keys }
1829                })
1830                .collect()
1831        });
1832        let mut out: Vec<(Vec<u32>, Symbol<'d>)> = Vec::new();
1833        for ((id, _), cc) in self.classes.iter().zip(chains) {
1834            if !cc.keys.contains(&decl_key) {
1835                continue;
1836            }
1837            let found = cc.chain.iter().rev().find_map(|c| {
1838                let s = c.as_symbol().find(name)?;
1839                let s = if s.kind() == SymbolKind::MethodPrototype {
1840                    s.method_prototype_subroutine()?
1841                } else {
1842                    s
1843                };
1844                (s.kind() == SymbolKind::Subroutine
1845                    && s.body().is_some()
1846                    && !s.subroutine_flags().contains(sv_lang::MethodFlags::PURE))
1847                .then_some(s)
1848            });
1849            let Some(imp) = found else { continue };
1850            match out.iter_mut().find(|(_, s)| *s == imp) {
1851                Some((ids, _)) => ids.push(*id),
1852                None => out.push((vec![*id], imp)),
1853            }
1854        }
1855        out
1856    }
1857}
1858
1859/// [`build_cfg`] for one branch of the fork block named `fork_name`, if
1860/// named: a `disable` of that name inside the branch ends the fork.
1861#[allow(clippy::too_many_arguments)]
1862fn build_cfg_in<'d>(
1863    body: &Statement<'d>,
1864    loop_kind: ProcessLoopKind,
1865    fork_name: Option<String>,
1866    default_clocking: Option<Symbol<'d>>,
1867    peers: Option<&[PeerBlock]>,
1868    checker_clock: Option<InferredClockEvent<'d>>,
1869    inherited: Inherited<'d>,
1870    virtual_tasks: Option<std::rc::Rc<VirtualTasks<'d>>>,
1871) -> Result<ProcessCfg<'d>, LoweringError> {
1872    // LRM 9.6.2: the name(s) a `disable` ANYWHERE may single this CFG's own
1873    // process out by -- computed before `fork_name` moves into `builder`.
1874    let mut disable_names: Vec<String> = Vec::new();
1875    disable_names.extend(fork_name.clone());
1876    disable_names.extend(peer_block(body).map(|p| p.name));
1877
1878    let mut builder = CfgBuilder::new();
1879    builder.this_stack.extend(inherited.this_var);
1880    builder.ref_stack = inherited.ref_aliases;
1881    builder.global_clock = inherited.global_clock;
1882    builder.detached_trigger = inherited.detached_trigger;
1883    builder.forced_extern = inherited.forced_extern;
1884    builder.rec_outer = inherited.rec_outer;
1885    if let Some(memo) = inherited.fork_memo {
1886        builder.fork_memo = memo;
1887    }
1888    if let Some(rec) = inherited.rec {
1889        // The same capture temporaries the activation reserves.
1890        for _ in 0..rec.groups {
1891            for &(a, f) in &rec.proto {
1892                builder.set_capture_type(a, f);
1893            }
1894        }
1895        builder.rec = Some(rec);
1896    }
1897    builder.rec_up = inherited.rec_up;
1898    builder.virtual_tasks = virtual_tasks;
1899    builder.fork_name = fork_name;
1900    builder.fork_branch_names = inherited.known_fork_labels;
1901    // LRM 9.6.2: a branch also answers to `disable` of any block enclosing
1902    // its fork (it is an activity enabled within that block), so a block that
1903    // ends with detached branches still running ends them. Only names some
1904    // `disable` of the design mentions are tagged.
1905    builder.disable_names_extra(&inherited.ancestor_blocks, &mut disable_names);
1906    builder.body_node = Some(body.as_sem_node());
1907    builder.ancestor_blocks = inherited.ancestor_blocks;
1908    builder.default_clocking = default_clocking;
1909    builder.checker_clock = checker_clock;
1910    if let Some(peers) = peers {
1911        // A fork branch is not the process's own scope: only a whole process
1912        // has an outermost scope, assertion labels and peers.
1913        builder.peers = peers.to_vec();
1914        builder.assertion_labels = assertion_labels(body)
1915            .into_iter()
1916            .map(|(name, _)| name)
1917            .collect();
1918        builder.deferred_labels = deferred_assertion_labels(body)
1919            .into_iter()
1920            .map(|(name, _)| name)
1921            .collect();
1922        builder.immediate_labels = immediate_assertion_labels(body);
1923        builder.outermost = peer_block(body).map(|p| p.name);
1924    }
1925    let entry = builder.alloc_block();
1926    let exit = builder.walk_stmt(*body, entry)?;
1927    let loop_back = match loop_kind {
1928        ProcessLoopKind::OneShot => None,
1929        ProcessLoopKind::Looping => Some(entry),
1930    };
1931    builder.close(exit, Terminator::End { loop_back });
1932    let ref_bindings = builder.take_ref_bindings();
1933    let inlined_locals = std::mem::take(&mut builder.inlined_locals);
1934    let captures = std::mem::take(&mut builder.captures);
1935    let capture_types = std::mem::take(&mut builder.capture_types);
1936    let fork_branch_names = std::mem::take(&mut builder.fork_branch_names);
1937    let disable_scopes = builder.take_disable_scopes();
1938    let disable_broadcast = std::mem::take(&mut builder.disable_broadcast);
1939    Ok(ProcessCfg {
1940        blocks: builder.finish(),
1941        entry,
1942        body: *body,
1943        inlined_locals,
1944        captures,
1945        captured: Vec::new(),
1946        ref_bindings,
1947        disable_names,
1948        fork_branch_names,
1949        disable_scopes,
1950        disable_broadcast,
1951        inherited: Vec::new(),
1952        capture_types,
1953        recursion: Recursion::None,
1954        share_id: None,
1955    })
1956}
1957
1958/// The recursive task whose activation a builder is walking.
1959#[derive(Clone)]
1960struct RecursiveTask<'d> {
1961    task: crate::symid::SymbolId,
1962    /// The [`ProcessCfg::share_id`] of this activation's Root (unique per
1963    /// distinct [`RootKey`]); part of the key of every Root nested in it.
1964    share: u64,
1965    /// The first call's actuals and the formals they shape (the capture
1966    /// temporaries' types, [`CfgBuilder::set_capture_type`]), so a fork
1967    /// branch inside the activation can reserve the same temporaries.
1968    proto: Vec<(Expression<'d>, Symbol<'d>)>,
1969    /// The number of formals: capture temporaries `0..n` hold the incoming
1970    /// arguments, `n..2n` the arguments of a recursive call.
1971    n: u32,
1972    /// The number of temporary groups of `n`: 2, or 4 when a formal is an
1973    /// `output`/`inout` (`2n..3n` the activation's results, `3n..4n` those
1974    /// of a recursive call).
1975    groups: u32,
1976    /// Per formal: whether it is an `output`/`inout`.
1977    outs: Vec<bool>,
1978    /// Per formal: the static storage an aliased `ref` formal names.
1979    refs: Vec<Option<Symbol<'d>>>,
1980    /// A class method: capture temporary 0 (formal 0) is its `this`.
1981    has_this: bool,
1982    body: Statement<'d>,
1983}
1984
1985/// Everything a fork branch's CFG is a function of besides its statement:
1986/// the key of [`CfgBuilder::fork_memo`]. A branch is walked by a fresh
1987/// builder seeded only from [`Inherited`], so equal keys give equal CFGs.
1988#[derive(PartialEq, Eq, Hash)]
1989struct ForkBranchKey {
1990    /// Address and kind of the branch statement's AST node.
1991    stmt: (usize, u16),
1992    fork_name: Option<String>,
1993    default_clocking: Option<crate::symid::SymbolId>,
1994    this_var: Option<crate::symid::SymbolId>,
1995    ref_aliases: Vec<(crate::symid::SymbolId, crate::symid::SymbolId)>,
1996    rec_outer: Vec<crate::symid::SymbolId>,
1997    labels: Vec<String>,
1998    ancestors: Vec<String>,
1999}
2000
2001/// Everything a [`Recursion::Root`] activation's CFG is a function of: the
2002/// key of its entry in [`CfgBuilder::fork_memo`]. A Root is walked by a fresh
2003/// builder seeded from exactly these fields (the call site's actuals only
2004/// supply per-site capture bindings; the capture temporaries are typed by
2005/// the formals), so equal keys give equal CFGs. `chain` is the
2006/// [`RecursiveTask::share`] ids of the enclosing activations, which pins
2007/// each one's CFG (and so its entry block and frame layout, which an
2008/// [`Recursion::AncestorSpawn`] inside this Root copies from).
2009#[derive(PartialEq, Eq, Hash)]
2010struct RootKey {
2011    task: crate::symid::SymbolId,
2012    this_var: Option<crate::symid::SymbolId>,
2013    default_clocking: Option<crate::symid::SymbolId>,
2014    rec_outer: Vec<crate::symid::SymbolId>,
2015    chain: Vec<u64>,
2016    ref_aliases: Vec<(crate::symid::SymbolId, crate::symid::SymbolId)>,
2017    labels: Vec<String>,
2018    ancestors: Vec<String>,
2019}
2020
2021/// A [`CfgBuilder::fork_memo`] key: a fork branch or a recursive-task Root.
2022#[derive(PartialEq, Eq, Hash)]
2023enum MemoKey {
2024    Fork(ForkBranchKey),
2025    Root(RootKey),
2026}
2027
2028/// The fork branches and recursive-task Roots of one process already built,
2029/// by [`MemoKey`].
2030type ForkMemo<'d> =
2031    std::rc::Rc<std::cell::RefCell<std::collections::HashMap<MemoKey, ProcessCfg<'d>>>>;
2032
2033/// Source of [`ProcessCfg::share_id`]s (unique across the program).
2034static NEXT_SHARE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
2035
2036/// The AST node address of a statement handle (a handle is a copy of the
2037/// FFI node pointer; two handles to one node compare equal here).
2038fn stmt_addr(s: &Statement<'_>) -> usize {
2039    // SAFETY: `Statement` wraps one `repr(C)` `slang_ast` whose first field
2040    // is the node pointer; reading that word reads the pointer.
2041    unsafe { *std::ptr::from_ref(s).cast::<usize>() }
2042}
2043
2044/// What a fork branch inherits from the inlined task it is forked in: its
2045/// `this` and its aliased `ref` formals.
2046#[derive(Default)]
2047struct Inherited<'d> {
2048    this_var: Option<Symbol<'d>>,
2049    ref_aliases: Vec<(Symbol<'d>, Symbol<'d>)>,
2050    /// [`CfgBuilder::global_clock`].
2051    global_clock: Option<InferredClockEvent<'d>>,
2052    /// [`CfgBuilder::detached_trigger`].
2053    detached_trigger: bool,
2054    /// [`CfgBuilder::forced_extern`].
2055    forced_extern: Option<Symbol<'d>>,
2056    /// [`CfgBuilder::rec_outer`].
2057    rec_outer: Vec<crate::symid::SymbolId>,
2058    /// [`CfgBuilder::fork_memo`], shared by a whole process's builders.
2059    fork_memo: Option<ForkMemo<'d>>,
2060    /// [`CfgBuilder::rec`]: a fork branch inside a recursive activation can
2061    /// itself call the task (each such call spawns the activation again).
2062    rec: Option<RecursiveTask<'d>>,
2063    /// [`CfgBuilder::rec_up`].
2064    rec_up: Vec<RecursiveTask<'d>>,
2065    /// [`CfgBuilder::fork_branch_names`]'s own starting point: every label
2066    /// already known reachable from here (the parent's own accumulated list
2067    /// at the fork site, plus this fork's sibling branches) before this
2068    /// CFG's own walk adds any it discovers deeper in its own body.
2069    known_fork_labels: Vec<String>,
2070    /// [`CfgBuilder::ancestor_blocks`]' starting point.
2071    ancestor_blocks: Vec<String>,
2072}
2073
2074/// A VPI frame probe of an inlined blocking task (37.43): its activation
2075/// begins (after its arguments and locals are set up) or ends.
2076#[derive(Clone, Debug)]
2077pub enum FrameProbe<'d> {
2078    Enter {
2079        task: Symbol<'d>,
2080        /// Its automatic variables: formals, then locals.
2081        vars: Vec<Symbol<'d>>,
2082        /// The `ref` formals the call bound by copy-in/copy-out (an actual
2083        /// that is not static storage) with their actuals, in formal order
2084        /// (37.15 detail 3). They are ref objs of the frame, not variables
2085        /// (so absent from `vars`).
2086        bound: Vec<(Symbol<'d>, Expression<'d>)>,
2087    },
2088    Exit {
2089        task: Symbol<'d>,
2090        vars: Vec<Symbol<'d>>,
2091    },
2092}
2093
2094struct PartialBlock<'d> {
2095    bindings: Vec<ArgBinding<'d>>,
2096    this_var: Option<Symbol<'d>>,
2097    ref_aliases: Vec<(Symbol<'d>, Symbol<'d>)>,
2098    stmts: Vec<Statement<'d>>,
2099    probes: Vec<(usize, Statement<'d>)>,
2100    disable_probes: Vec<(usize, Statement<'d>)>,
2101    frame_probes: Vec<(usize, FrameProbe<'d>)>,
2102    block_events: Vec<(usize, Symbol<'d>, bool)>,
2103    stmt_scopes: Vec<(usize, Symbol<'d>)>,
2104    step_exprs: Vec<Expression<'d>>,
2105    repeat_init: Option<(RepeatCounterId, RepeatCount<'d>)>,
2106    repeat_decrement: Option<RepeatCounterId>,
2107    expect_resume: bool,
2108    terminator: Option<Terminator<'d>>,
2109}
2110
2111struct CfgBuilder<'d> {
2112    blocks: Vec<PartialBlock<'d>>,
2113    /// Task 1: the single clock signal+edge `##N` (`CycleDelay`) is lowered
2114    /// against, inferred from the first `posedge`/`negedge` `SuspendPoint::
2115    /// Edge` this walk encounters (in practice, the enclosing `always_ff`'s
2116    /// own edge). `None` until seen; becomes permanently ambiguous
2117    /// (`Some(None)`) the moment a *second*, differently-referenced edge
2118    /// signal (or the SAME signal with a different edge -- Task 6 extends
2119    /// the ambiguity check to cover this too, since `CycleDelay` now carries
2120    /// a real `edge` field it must be unambiguous about) is seen, so a later
2121    /// `##N` inside a multi-clock or unclocked-context process is a named
2122    /// `LoweringError`, never a silent guess (`walk_timed`'s `CycleDelay`
2123    /// arm). Task 6: also threaded through as real `SuspendPoint::
2124    /// CycleDelay{clock, edge, ..}` payload -- previously computed
2125    /// then discarded once the ambiguity check passed.
2126    inferred_clock: Option<Option<(Expression<'d>, EdgeKind)>>,
2127    /// R-run Task 8: the next fresh [`RepeatCounterId`] this builder will
2128    /// mint -- dense, allocation-order-numbered, mirrors [`Self::blocks`]'s
2129    /// own `len()`-as-next-id discipline for [`BlockId`].
2130    next_repeat_counter: u32,
2131    /// Formals/locals of every task inlined so far (`ProcessCfg::
2132    /// inlined_locals`).
2133    inlined_locals: Vec<Symbol<'d>>,
2134    /// Every inlined call's `ref` formal with the symbol its actual names
2135    /// (`None` for an actual that is not a plain name): resolved into
2136    /// `ProcessCfg::ref_bindings` once the whole process is walked.
2137    ref_candidates: Vec<(Symbol<'d>, Option<Symbol<'d>>)>,
2138    /// Set while walking a block whose automatic variables were just
2139    /// bound on entry (so the block itself is walked normally).
2140    entering_block: bool,
2141    /// The call being inlined, whose type shapes the `this` capture of a
2142    /// recursive method call with an implicit receiver.
2143    call_anchor: Option<Expression<'d>>,
2144    /// The capture temporary the next [`Self::inline_task_call`] parks the
2145    /// called function's result in ([`ArgBinding::CallResult`]).
2146    pending_result_temp: Option<(u32, Expression<'d>)>,
2147    /// Where a `return;` inside the innermost task being inlined goes.
2148    return_targets: Vec<BlockId>,
2149    /// The result variable of each inlined function (`None` for a task or void
2150    /// function), parallel to the inlined entries of [`Self::return_targets`]:
2151    /// `return expr;` assigns it (LRM 1800-2023 13.4.1).
2152    ret_var_stack: Vec<Option<Symbol<'d>>>,
2153    /// The `this` of each class-method task being inlined, innermost last
2154    /// ([`BasicBlock::this_var`]).
2155    this_stack: Vec<Symbol<'d>>,
2156    /// The `ref` formals of the tasks being inlined that alias their call's
2157    /// storage ([`BasicBlock::ref_aliases`]), innermost last.
2158    ref_stack: Vec<(Symbol<'d>, Symbol<'d>)>,
2159    /// The class templates a virtual suspending task can dispatch over.
2160    virtual_tasks: Option<std::rc::Rc<VirtualTasks<'d>>>,
2161    /// The type source of each capture temporary ([`ProcessCfg::capture_types`]).
2162    capture_types: Vec<Option<Symbol<'d>>>,
2163    /// Tasks currently being inlined (recursion guard).
2164    inline_stack: Vec<crate::symid::SymbolId>,
2165    /// While building a recursive task's activation
2166    /// ([`Self::inline_recursive_task`]): the task, whose calls are forks.
2167    rec: Option<RecursiveTask<'d>>,
2168    /// The recursive activations enclosing [`Self::rec`]'s (outermost first):
2169    /// a call of one of their tasks is a spawn of that activation
2170    /// ([`Recursion::AncestorSpawn`]).
2171    rec_up: Vec<RecursiveTask<'d>>,
2172    /// The recursive tasks whose activation encloses this builder's CFG (a
2173    /// fork branch inside one cannot start another activation of it).
2174    rec_outer: Vec<crate::symid::SymbolId>,
2175    /// The innermost enclosing loop's `continue` and `break` targets.
2176    loop_targets: Vec<(BlockId, BlockId)>,
2177    /// The sequential blocks being walked that declare automatic variables
2178    /// holding class or collection handles, innermost last (LRM 6.21: such a
2179    /// variable ends with its block, and a handle is released then).
2180    owned_scopes: Vec<Vec<Symbol<'d>>>,
2181    /// For each jump target (`break`/`continue`/`return`/`disable` exit), how
2182    /// many [`Self::owned_scopes`] were open outside it: a jump to it
2183    /// releases the scopes above that depth ([`Self::jump`]).
2184    target_depth: std::collections::HashMap<BlockId, usize>,
2185    /// The enclosing named blocks a `disable` may leave: `(name, exit)`,
2186    /// innermost last.
2187    /// `(label, exit, frames)`: `frames` is how many inlined tasks were
2188    /// active (`frame_stack`) when the block was entered.
2189    disable_targets: Vec<(String, BlockId, usize)>,
2190    /// The body this builder walks, for the lazy [`Self::body_forks`] scan.
2191    body_node: Option<SemNode<'d>>,
2192    /// The names of the fork blocks this builder walked (a subset of
2193    /// [`Self::fork_branch_names`]): a `fl.b.x` path through one is an
2194    /// instance-path-style label, not a branch name.
2195    fork_block_names: Vec<String>,
2196    /// Whether the body (or a task it calls) holds a fork block: decided on
2197    /// the first `disable` that jumps out of a named block, since only then
2198    /// can detached branches need ending with it (LRM 9.6.2).
2199    body_forks: Option<bool>,
2200    /// The covergroup-event blocks and inlined tasks being walked, innermost
2201    /// last, each with `disable_targets.len()` when it was entered: a
2202    /// `disable` that jumps out of them ends them there (LRM 9.6.2, 19.3), so
2203    /// [`Self::left_block_events`] signals their `end` at the jump.
2204    event_stack: Vec<(Symbol<'d>, usize)>,
2205    /// The tasks being inlined, innermost last, with their automatic
2206    /// variables (for the VPI frame probes a `disable` jumping out of them
2207    /// owes).
2208    frame_stack: Vec<(Symbol<'d>, Vec<Symbol<'d>>)>,
2209    /// The named blocks and inlined task activations being walked whose name
2210    /// some `disable` of the design targets, `(label, exit)`, innermost last:
2211    /// every block allocated while one is open is inside it
2212    /// ([`Self::block_scopes`], [`ProcessCfg::disable_scopes`]).
2213    /// `frames` is how many inlined tasks were active ([`Self::frame_stack`])
2214    /// outside the scope.
2215    scope_chain: Vec<(String, BlockId, usize)>,
2216    /// The names of the named sequential blocks being walked, outermost first
2217    /// (the path `disable p.b.x;` names, LRM 9.6.2).
2218    name_path: Vec<String>,
2219    /// [`Self::scope_chain`] as it was when each block was allocated.
2220    block_scopes: Vec<Vec<(String, BlockId, usize)>>,
2221    /// [`Self::frame_stack`] as it was when each block was allocated (empty
2222    /// outside every scope).
2223    block_frames: Vec<Vec<(Symbol<'d>, Vec<Symbol<'d>>)>>,
2224    /// [`ProcessCfg::disable_broadcast`].
2225    disable_broadcast: Vec<(String, bool)>,
2226    /// When this CFG is one branch of a named fork block: that name, which
2227    /// a `disable` inside it may target (`sv_rt_disable_fork_block`).
2228    fork_name: Option<String>,
2229    /// Every fork-branch [`ProcessCfg::disable_names`] a `disable <label>;`
2230    /// written anywhere in THIS CFG's own remaining walk may legitimately
2231    /// target (LRM 9.6.2), besides a same-process lexically enclosing block
2232    /// or this CFG's own `fork_name`: seeded from [`Inherited::
2233    /// known_fork_labels`] (what the parent already knew, plus this fork's
2234    /// siblings, when this CFG is itself a branch), then extended, going
2235    /// forward, by every later fork THIS walk itself builds branches for --
2236    /// never popped (unlike `disable_targets`): LRM 9.6.2 lets a branch's
2237    /// name be disabled for the rest of the process's life, not only while
2238    /// the `fork` statement that spawned it is still being walked.
2239    fork_branch_names: Vec<String>,
2240    /// The named blocks enclosing the fork statement(s) this CFG is a branch of,
2241    /// in the forking process(es) (LRM 9.6.2): a `disable` of one from inside
2242    /// the branch ends that block's whole activity, the forking process
2243    /// suspended at the join included, which resumes after the block.
2244    ancestor_blocks: Vec<String>,
2245    /// The name of the process's outermost scope (LRM 16.14.6.4), if it is a
2246    /// named block: a `disable` of it flushes the pending procedural assertion
2247    /// queue.
2248    outermost: Option<String>,
2249    /// The labels of the procedural concurrent assertions of this process: a
2250    /// `disable <label>;` clears the pending instances of that assertion
2251    /// (16.14.6.4).
2252    assertion_labels: Vec<String>,
2253    /// Labels of the process's deferred immediate assertions: `disable L;`
2254    /// cancels their pending reports (16.4.4), codegen lowers the kept statement.
2255    deferred_labels: Vec<String>,
2256    /// Labels of the process's plain immediate assertions: `disable L;` from
2257    /// outside the labeled statement is a no-op (it already finished).
2258    immediate_labels: Vec<String>,
2259    /// The outermost named blocks of the instance's other processes, which this
2260    /// process may `disable` (16.14.6.4, the `b3` example).
2261    peers: Vec<PeerBlock>,
2262    /// [`ProcessCfg::captures`].
2263    captures: Vec<Expression<'d>>,
2264    /// The instance's default clocking block, which `##N` counts.
2265    default_clocking: Option<Symbol<'d>>,
2266    /// While lowering the timing control of a clocking drive (`cb.q <= ##N v`):
2267    /// the drive target's own clocking block, whose event `##N` counts (LRM
2268    /// 1800-2023 14.16) instead of the default clocking's.
2269    drive_clocking: Option<Symbol<'d>>,
2270    /// The clock variable (`v.sb.c`) of that drive when it goes through a
2271    /// virtual interface member access, else `None` (see
2272    /// [`SuspendPoint::CycleDelay`]'s `via`).
2273    drive_via: Option<Expression<'d>>,
2274    /// Set by [`Self::walk_timed`] around its [`Self::suspend_point`] call: a
2275    /// run-time `##(expr)` count is then accepted (as a single `##1` wait the
2276    /// caller wraps in a counted loop) and its expression left in
2277    /// `dynamic_cycle_count`; every other caller still refuses it by name.
2278    allow_dynamic_cycle: bool,
2279    /// Set by [`Self::walk_timed`] around the [`Self::suspend_point`] of a
2280    /// mixed class-event / signal `@(a or b)` list: the class terms are then
2281    /// skipped (the leaf statement queues the process on them) and only the
2282    /// signal terms form the suspend point. Every other caller keeps the class
2283    /// term, so it cannot be dropped silently.
2284    skip_class_event_terms: bool,
2285    dynamic_cycle_count: Option<Expression<'d>>,
2286    /// What a checker procedure's `@clock` waits on when `clock` is the default
2287    /// `$inferred_clock` ([`InferredClockEvent`]).
2288    checker_clock: Option<InferredClockEvent<'d>>,
2289    /// What `@$global_clock` waits on: the event of the global clocking in
2290    /// effect for the process (14.14), when one is declared in its instance.
2291    global_clock: Option<InferredClockEvent<'d>>,
2292    /// Walking the body of the plan that hosts an `expect` statement for the
2293    /// assertion engine ([`build_cfg_hosted_assertion`]): the `expect` is then
2294    /// an ordinary leaf, not a blocking wait.
2295    expect_as_leaf: bool,
2296    /// This CFG is the detached process of a `->> @(ctl) e;` (LRM 1800-2023
2297    /// 15.5.2): its body statement is that trigger, which waits on `ctl` and
2298    /// then stages the nonblocking trigger, while the issuing process
2299    /// continues. Cleared once the body statement is walked.
2300    detached_trigger: bool,
2301    /// The fork branches this process has built so far ([`ForkBranchKey`]):
2302    /// a task inlined at many call sites repeats the same forks, and each
2303    /// distinct branch is built (and later lowered) once.
2304    fork_memo: ForkMemo<'d>,
2305    /// This CFG is one branch of a call to an `extern forkjoin` task (LRM
2306    /// 25.7.5): its body statement is that call, and it runs this one
2307    /// implementation. Cleared once the body statement is walked.
2308    forced_extern: Option<Symbol<'d>>,
2309}
2310
2311impl<'d> CfgBuilder<'d> {
2312    fn new() -> Self {
2313        Self {
2314            expect_as_leaf: false,
2315            detached_trigger: false,
2316            fork_memo: ForkMemo::default(),
2317            forced_extern: None,
2318            blocks: Vec::new(),
2319            inferred_clock: None,
2320            next_repeat_counter: 0,
2321            inlined_locals: Vec::new(),
2322            ref_candidates: Vec::new(),
2323            entering_block: false,
2324            return_targets: Vec::new(),
2325            ret_var_stack: Vec::new(),
2326            this_stack: Vec::new(),
2327            ref_stack: Vec::new(),
2328            virtual_tasks: None,
2329            inline_stack: Vec::new(),
2330            capture_types: Vec::new(),
2331            rec: None,
2332            rec_up: Vec::new(),
2333            rec_outer: Vec::new(),
2334            loop_targets: Vec::new(),
2335            owned_scopes: Vec::new(),
2336            target_depth: std::collections::HashMap::new(),
2337            disable_targets: Vec::new(),
2338            body_node: None,
2339            fork_block_names: Vec::new(),
2340            body_forks: None,
2341            event_stack: Vec::new(),
2342            call_anchor: None,
2343            pending_result_temp: None,
2344            frame_stack: Vec::new(),
2345            scope_chain: Vec::new(),
2346            name_path: Vec::new(),
2347            block_scopes: Vec::new(),
2348            block_frames: Vec::new(),
2349            disable_broadcast: Vec::new(),
2350            ancestor_blocks: Vec::new(),
2351            fork_name: None,
2352            fork_branch_names: Vec::new(),
2353            outermost: None,
2354            assertion_labels: Vec::new(),
2355            deferred_labels: Vec::new(),
2356            immediate_labels: Vec::new(),
2357            peers: Vec::new(),
2358            captures: Vec::new(),
2359            default_clocking: None,
2360            drive_clocking: None,
2361            drive_via: None,
2362            allow_dynamic_cycle: false,
2363            skip_class_event_terms: false,
2364            dynamic_cycle_count: None,
2365            checker_clock: None,
2366            global_clock: None,
2367        }
2368    }
2369
2370    fn alloc_block(&mut self) -> BlockId {
2371        let id = BlockId(self.blocks.len() as u32);
2372        let this_var = self.this_stack.last().copied();
2373        let ref_aliases = self.ref_stack.clone();
2374        self.block_scopes.push(self.scope_chain.clone());
2375        self.block_frames.push(if self.scope_chain.is_empty() {
2376            Vec::new()
2377        } else {
2378            self.frame_stack.clone()
2379        });
2380        self.blocks.push(PartialBlock {
2381            bindings: Vec::new(),
2382            this_var,
2383            ref_aliases,
2384            stmts: Vec::new(),
2385            probes: Vec::new(),
2386            disable_probes: Vec::new(),
2387            frame_probes: Vec::new(),
2388            block_events: Vec::new(),
2389            stmt_scopes: Vec::new(),
2390            step_exprs: Vec::new(),
2391            repeat_init: None,
2392            repeat_decrement: None,
2393            expect_resume: false,
2394            terminator: None,
2395        });
2396        id
2397    }
2398
2399    /// After a statement that called a subroutine, in a design where some
2400    /// function has `disable <block>;` (LRM 9.6.2, 35.9): leaves the enclosing
2401    /// named block the call disabled, if any ([`SwitchOn::FnDisable`]).
2402    /// Returns the block the statements go on in.
2403    fn fn_disable_check(&mut self, cur: BlockId) -> BlockId {
2404        let targets: Vec<(usize, String, BlockId, usize)> = self
2405            .disable_targets
2406            .iter()
2407            .enumerate()
2408            .rev()
2409            .filter(|(_, (n, _, _))| is_function_disable_label(n))
2410            .map(|(pos, (n, exit, frames))| (pos, n.clone(), *exit, *frames))
2411            .collect();
2412        if targets.is_empty() {
2413            return cur;
2414        }
2415        let next = self.alloc_block();
2416        let mut arms = Vec::new();
2417        let mut labels = Vec::new();
2418        for (pos, label, exit, frames) in targets {
2419            let arm = self.alloc_block();
2420            // The inlined tasks the jump leaves end here, innermost first.
2421            let left: Vec<_> = self.frame_stack[frames..].iter().rev().cloned().collect();
2422            for (task, vars) in left {
2423                self.frame_probe(arm, FrameProbe::Exit { task, vars });
2424            }
2425            self.left_block_events(arm, pos);
2426            self.jump(arm, exit);
2427            arms.push(arm);
2428            labels.push(label);
2429        }
2430        self.close(
2431            cur,
2432            Terminator::Switch {
2433                on: SwitchOn::FnDisable { labels },
2434                arms,
2435                default_blk: next,
2436                report: None,
2437            },
2438        );
2439        next
2440    }
2441
2442    fn push(&mut self, blk: BlockId, stmt: Statement<'d>) {
2443        let scope = self.frame_stack.last().map(|f| f.0);
2444        let b = &mut self.blocks[blk.0 as usize];
2445        if let Some(scope) = scope {
2446            b.stmt_scopes.push((b.stmts.len(), scope));
2447        }
2448        b.stmts.push(stmt);
2449    }
2450
2451    /// Records that `stmt` is about to execute at the current end of block
2452    /// `blk`'s statements ([`BasicBlock::probes`]).
2453    fn probe(&mut self, blk: BlockId, stmt: Statement<'d>) {
2454        let b = &mut self.blocks[blk.0 as usize];
2455        let at = b.stmts.len();
2456        b.probes.push((at, stmt));
2457    }
2458
2459    /// Records that the `disable` statement `stmt`, turned into a jump, has
2460    /// taken effect at the current end of block `blk`'s statements
2461    /// ([`BasicBlock::disable_probes`]).
2462    fn disable_probe(&mut self, blk: BlockId, stmt: Statement<'d>) {
2463        let b = &mut self.blocks[blk.0 as usize];
2464        let at = b.stmts.len();
2465        b.disable_probes.push((at, stmt));
2466    }
2467
2468    /// Appends to `out` the `blocks` some `disable` of the design names (a
2469    /// fork branch's extra [`ProcessCfg::disable_names`]).
2470    fn disable_names_extra(&self, blocks: &[String], out: &mut Vec<String>) {
2471        for b in blocks {
2472            if is_disabled_somewhere(b) && !out.contains(b) {
2473                out.push(b.clone());
2474            }
2475        }
2476    }
2477
2478    /// Whether the body being walked holds a fork block, in itself or in a
2479    /// task it calls (cached).
2480    fn forks_in_body(&mut self) -> bool {
2481        if let Some(v) = self.body_forks {
2482            return v;
2483        }
2484        let v = self.body_node.is_some_and(contains_fork_through_calls);
2485        self.body_forks = Some(v);
2486        v
2487    }
2488
2489    /// `disable a.b;` (LRM 9.6.2) whose segments are a contiguous chain of
2490    /// this process's lexically enclosing named blocks: a jump to the last
2491    /// one's exit, exactly like `disable b;`. Any other hierarchical target
2492    /// (an instance path, a block not enclosing the statement, or the
2493    /// outermost block / a task frame, whose lowering needs the plain label)
2494    /// stays loud.
2495    fn walk_hier_disable_of_enclosing(
2496        &mut self,
2497        stmt: Statement<'d>,
2498        cur: BlockId,
2499        path: &[String],
2500    ) -> Result<Option<BlockId>, LoweringError> {
2501        let loud = || {
2502            LoweringError::unsupported(format!(
2503                "disable of `{}` (a hierarchical name is supported only as a chain of the \
2504                 statement's own lexically enclosing named blocks, not the process's \
2505                 outermost block or a task frame; IEEE 1800-2023 9.6.2)",
2506                path.join(".")
2507            ))
2508        };
2509        let last = path.last().ok_or_else(loud)?;
2510        let Some(pos) = self.disable_targets.iter().rposition(|(n, _, _)| n == last) else {
2511            return Ok(None);
2512        };
2513        let chain_ok = pos + 1 >= path.len()
2514            && path
2515                .iter()
2516                .enumerate()
2517                .all(|(i, seg)| self.disable_targets[pos + 1 - path.len() + i].0 == *seg);
2518        if !chain_ok {
2519            return Ok(None);
2520        }
2521        let (_, exit, frames) = self.disable_targets[pos].clone();
2522        let outermost = pos == 0 && self.outermost.as_deref() == Some(last.as_str());
2523        if outermost || frames > 0 {
2524            return Err(loud());
2525        }
2526        self.disable_probe(cur, stmt);
2527        self.left_block_events(cur, pos);
2528        self.jump(cur, exit);
2529        Ok(Some(self.alloc_block()))
2530    }
2531
2532    /// The target of `disable a.b.c;` (LRM 9.6.2) when every segment names a
2533    /// block of ANOTHER process of this instance ([`PeerBlock`]): the last
2534    /// segment, handled like the plain `disable c;` of that peer. An instance
2535    /// path (or any segment that is not a peer block) stays loud, since the
2536    /// runtime resolves a label within the requester's own instance only.
2537    fn hier_peer_leaf(&self, path: &[String]) -> Result<String, LoweringError> {
2538        if path
2539            .iter()
2540            .all(|seg| self.peers.iter().any(|p| &p.name == seg))
2541        {
2542            return Ok(path[path.len() - 1].clone());
2543        }
2544        // `disable outer.inner;` / `disable top.outer.inner;` where `inner` is
2545        // a fork branch's own named block (a sibling or a later branch of this
2546        // process) or a block enclosing the fork, reached through blocks of
2547        // this process family (LRM 9.6.2): the same target as the plain
2548        // `disable inner;`. A leading segment that names none of them is the
2549        // instance's own name (only when a block segment follows it). Left as
2550        // the dotted text it would name a block of a child instance and match
2551        // nothing, a silent no-op.
2552        let known = |s: &String| {
2553            self.ancestor_blocks.contains(s)
2554                || self.fork_branch_names.contains(s)
2555                || self.disable_targets.iter().any(|(n, _, _)| n == s)
2556                || self.outermost.as_deref() == Some(s.as_str())
2557        };
2558        if let Some((leaf, mid)) = path.split_last()
2559            && known(leaf)
2560        {
2561            let skip = usize::from(mid.len() >= 2 && !known(&mid[0]));
2562            if mid[skip..].iter().all(known) {
2563                return Ok(leaf.clone());
2564            }
2565        }
2566        // `disable u1.blk;` (an instance path): the first segment names
2567        // neither a block of this or another process nor a task, so it is a
2568        // child instance. Kept as the dotted text; the runtime resolves it from
2569        // the requester's instance (the extra scope keys run.rs registers).
2570        let first = &path[0];
2571        if !self.peers.iter().any(|p| &p.name == first)
2572            && !self.disable_targets.iter().any(|(n, _, _)| n == first)
2573            && !is_task_name(first)
2574            && self.fork_name.as_deref() != Some(first.as_str())
2575            && !self
2576                .fork_branch_names
2577                .iter()
2578                .any(|n| n == first && !self.fork_block_names.contains(n))
2579        {
2580            return Ok(path.join("."));
2581        }
2582        // `disable p.b.x;` where `x` also names a block elsewhere: the dotted
2583        // path from another process's outermost block `p`, which the runtime
2584        // finds under the path-qualified scope label (`CfgBuilder::name_path`).
2585        if self.peers.iter().any(|p| &p.name == first) {
2586            return Ok(path.join("."));
2587        }
2588        Err(LoweringError::unsupported(format!(
2589            "disable of `{}` (a hierarchical name is supported only as a chain of the \
2590             statement's own lexically enclosing named blocks or of the named blocks of \
2591             another process of the same instance, not an instance path, the process's \
2592             outermost block or a task frame; IEEE 1800-2023 9.6.2)",
2593            path.join(".")
2594        )))
2595    }
2596
2597    /// Records the frame probe `probe` at the current end of block `blk`'s
2598    /// statements ([`BasicBlock::frame_probes`]).
2599    fn frame_probe(&mut self, blk: BlockId, probe: FrameProbe<'d>) {
2600        let b = &mut self.blocks[blk.0 as usize];
2601        let at = b.stmts.len();
2602        b.frame_probes.push((at, probe));
2603    }
2604
2605    /// Records the covergroup block event of `task` (LRM 19.3) at the current
2606    /// end of block `blk`'s statements ([`BasicBlock::block_events`]).
2607    fn block_event(&mut self, blk: BlockId, task: Symbol<'d>, is_end: bool) {
2608        let b = &mut self.blocks[blk.0 as usize];
2609        let at = b.stmts.len();
2610        b.block_events.push((at, task, is_end));
2611    }
2612
2613    /// A `disable` jumping to `disable_targets[pos]` ends every block and
2614    /// inlined task entered inside that target (LRM 9.6.2): records their
2615    /// `end` covergroup events (LRM 19.3) at the jump, innermost first. The
2616    /// target's own end event is the one at its exit block.
2617    fn left_block_events(&mut self, cur: BlockId, pos: usize) {
2618        let left: Vec<_> = self
2619            .event_stack
2620            .iter()
2621            .rev()
2622            .take_while(|(_, depth)| *depth > pos)
2623            .map(|(s, _)| *s)
2624            .collect();
2625        for s in left {
2626            self.block_event(cur, s, true);
2627        }
2628    }
2629
2630    fn push_step_expr(&mut self, blk: BlockId, expr: Expression<'d>) {
2631        self.blocks[blk.0 as usize].step_exprs.push(expr);
2632    }
2633
2634    /// R-run Task 8: mints a fresh, process-unique [`RepeatCounterId`] for a
2635    /// suspend-containing `repeat(N)` loop's own synthetic counter.
2636    fn alloc_repeat_counter(&mut self) -> RepeatCounterId {
2637        let id = RepeatCounterId(self.next_repeat_counter);
2638        self.next_repeat_counter += 1;
2639        id
2640    }
2641
2642    /// R-run Task 8: records `blk`'s own `repeat_init` -- see
2643    /// [`BasicBlock::repeat_init`]'s doc comment. `blk` must not already
2644    /// carry one (a builder-internal consistency check, like [`Self::close`]'s
2645    /// own "not already closed" invariant: every call site is
2646    /// `walk_repeat_loop`'s own `cur`, which allocates/owns exactly one
2647    /// `repeat_init` per call).
2648    fn set_repeat_init(&mut self, blk: BlockId, counter: RepeatCounterId, count: RepeatCount<'d>) {
2649        let slot = &mut self.blocks[blk.0 as usize].repeat_init;
2650        debug_assert!(
2651            slot.is_none(),
2652            "cfg builder tried to set block {blk:?}'s repeat_init twice"
2653        );
2654        *slot = Some((counter, count));
2655    }
2656
2657    /// R-run Task 8: records `blk`'s own `repeat_decrement` -- see
2658    /// [`BasicBlock::repeat_decrement`]'s doc comment.
2659    fn set_repeat_decrement(&mut self, blk: BlockId, counter: RepeatCounterId) {
2660        let slot = &mut self.blocks[blk.0 as usize].repeat_decrement;
2661        debug_assert!(
2662            slot.is_none(),
2663            "cfg builder tried to set block {blk:?}'s repeat_decrement twice"
2664        );
2665        *slot = Some(counter);
2666    }
2667
2668    /// Records `signal`/`edge` (from a just-built `SuspendPoint::Edge`) as
2669    /// the process's inferred single clock -- see `inferred_clock`'s own doc
2670    /// comment. Compares *by referenced symbol identity*, since two
2671    /// `Expression` handles for the same `clk` are not otherwise comparable;
2672    /// an edge with no resolvable symbol (e.g. a bit-select or expression,
2673    /// not a plain name) can never disambiguate a later `##N`, so it does
2674    /// not overwrite an already-known clock but also never establishes one
2675    /// on its own. Task 6: a second sighting of the SAME signal with a
2676    /// DIFFERENT edge also becomes ambiguous now (`CycleDelay` carries a
2677    /// real `edge` field it must be unambiguous about, unlike the
2678    /// pre-Task-6 bare cycle count).
2679    fn note_edge_signal(&mut self, signal: Expression<'d>, edge: EdgeKind) {
2680        let this_sym = signal.referenced_symbol().map(|s| s.uid());
2681        match &self.inferred_clock {
2682            None => {
2683                self.inferred_clock = Some(this_sym.is_some().then_some((signal, edge)));
2684            }
2685            Some(Some((existing, existing_edge))) => {
2686                let existing_sym = existing.referenced_symbol().map(|s| s.uid());
2687                if existing_sym.is_none() || this_sym != existing_sym || *existing_edge != edge {
2688                    self.inferred_clock = Some(None); // now ambiguous
2689                }
2690            }
2691            Some(None) => {} // already ambiguous; stays ambiguous
2692        }
2693    }
2694
2695    /// Closes `blk` with `term`. `blk` must not already be closed -- every
2696    /// call site in this file only ever passes a `BlockId` it just allocated
2697    /// or one a nested `walk_stmt` call returned (which is itself always
2698    /// still-open by the same invariant), so this is a builder-internal
2699    /// consistency check, not a condition reachable from malformed SV source.
2700    fn close(&mut self, blk: BlockId, term: Terminator<'d>) {
2701        let slot = &mut self.blocks[blk.0 as usize].terminator;
2702        debug_assert!(
2703            slot.is_none(),
2704            "cfg builder tried to close block {blk:?} twice"
2705        );
2706        *slot = Some(term);
2707    }
2708
2709    /// Closes `cur` with a jump to `target`, releasing on the way the handles
2710    /// of the block-scoped automatic variables the jump leaves (a `break`,
2711    /// `continue`, `return` or `disable` out of a block, LRM 6.21).
2712    fn jump(&mut self, cur: BlockId, target: BlockId) {
2713        let open = self.owned_scopes.len();
2714        let depth = self.target_depth.get(&target).copied().unwrap_or(open);
2715        let vars: Vec<Symbol<'d>> = self.owned_scopes[depth.min(open)..]
2716            .iter()
2717            .rev()
2718            .flat_map(|s| s.iter().rev().copied())
2719            .collect();
2720        if vars.is_empty() {
2721            self.close(cur, Terminator::Fallthrough(target));
2722            return;
2723        }
2724        let leave = self.alloc_block();
2725        self.close(cur, Terminator::Fallthrough(leave));
2726        for var in vars {
2727            self.blocks[leave.0 as usize]
2728                .bindings
2729                .push(ArgBinding::Release { var });
2730        }
2731        self.close(leave, Terminator::Fallthrough(target));
2732    }
2733
2734    /// The static variable or net a `ref` actual names: itself, or what an
2735    /// enclosing inlined call's aliased `ref` formal it names stands for.
2736    fn ref_storage(&self, actual: Expression<'d>) -> Option<Symbol<'d>> {
2737        if !crate::symid::is_value_name(actual.kind()) {
2738            return None;
2739        }
2740        let sym = actual.referenced_symbol()?;
2741        if sym.kind() == SymbolKind::Net
2742            || (sym.kind() == SymbolKind::Variable
2743                && sym.variable_lifetime() == sv_lang::VariableLifetime::Static)
2744        {
2745            return Some(sym);
2746        }
2747        self.ref_stack
2748            .iter()
2749            .rev()
2750            .find(|(f, _)| f.uid() == sym.uid())
2751            .map(|&(_, a)| a)
2752    }
2753
2754    /// Resolves [`Self::ref_candidates`] into `ProcessCfg::ref_bindings`: a
2755    /// `ref` formal every call binds to one static variable or net (directly,
2756    /// or through an enclosing inlined task's `ref` formal that is itself
2757    /// bound) names that storage, so its copy-in/copy-out bindings and its
2758    /// frame local are dropped. A formal bound to different actuals, or to
2759    /// an automatic variable or a select, keeps the copy semantics.
2760    fn take_ref_bindings(&mut self) -> Vec<(Symbol<'d>, Symbol<'d>)> {
2761        use std::collections::HashMap;
2762        let storage = |s: &Symbol<'d>| {
2763            s.kind() == SymbolKind::Net
2764                || (s.kind() == SymbolKind::Variable
2765                    && s.variable_lifetime() == sv_lang::VariableLifetime::Static)
2766        };
2767        // formal uid -> (formal, the one target symbol every call names), or
2768        // None once two calls disagree or one names no plain symbol.
2769        let mut targets: HashMap<crate::symid::SymbolId, (Symbol<'d>, Option<Symbol<'d>>)> =
2770            HashMap::new();
2771        let mut order = Vec::new();
2772        for (formal, named) in &self.ref_candidates {
2773            match targets.get_mut(&formal.uid()) {
2774                None => {
2775                    order.push(formal.uid());
2776                    targets.insert(formal.uid(), (*formal, *named));
2777                }
2778                Some((_, cur)) => {
2779                    if cur.map(|c| c.uid()) != named.map(|n| n.uid()) {
2780                        *cur = None;
2781                    }
2782                }
2783            }
2784        }
2785        // Follow a chain through enclosing tasks' bound `ref` formals.
2786        let resolve = |start: Symbol<'d>| -> Option<Symbol<'d>> {
2787            let mut cur = start;
2788            for _ in 0..=targets.len() {
2789                if storage(&cur) {
2790                    return Some(cur);
2791                }
2792                cur = targets.get(&cur.uid())?.1?;
2793            }
2794            None
2795        };
2796        let mut out = Vec::new();
2797        for uid in order {
2798            let (formal, named) = targets[&uid];
2799            if let Some(actual) = named.and_then(resolve) {
2800                out.push((formal, actual));
2801            }
2802        }
2803        let bound: std::collections::HashSet<crate::symid::SymbolId> =
2804            out.iter().map(|(f, _)| f.uid()).collect();
2805        for b in &mut self.blocks {
2806            b.bindings.retain(|x| match x {
2807                ArgBinding::In { formal, .. } | ArgBinding::Out { formal, .. } => {
2808                    !bound.contains(&formal.uid())
2809                }
2810                _ => true,
2811            });
2812        }
2813        self.inlined_locals.retain(|l| !bound.contains(&l.uid()));
2814        out
2815    }
2816
2817    /// [`ProcessCfg::disable_scopes`] from the scopes each block was allocated in.
2818    ///
2819    /// A resume point inside inlined tasks the disabled scope does not contain
2820    /// goes through a block of its own that ends their VPI frames first,
2821    /// innermost first (37.43), like a `disable` jump inside the process does.
2822    fn take_disable_scopes(&mut self) -> Vec<(BlockId, String, BlockId)> {
2823        let scopes = std::mem::take(&mut self.block_scopes);
2824        let frames = std::mem::take(&mut self.block_frames);
2825        let mut out = Vec::new();
2826        for (i, (chain, active)) in scopes.into_iter().zip(frames).enumerate() {
2827            for (label, exit, outside) in chain {
2828                let target = if active.len() > outside {
2829                    let leave = self.alloc_block();
2830                    for (task, vars) in active[outside..].iter().rev().cloned() {
2831                        self.frame_probe(leave, FrameProbe::Exit { task, vars });
2832                    }
2833                    self.close(leave, Terminator::Fallthrough(exit));
2834                    leave
2835                } else {
2836                    exit
2837                };
2838                out.push((BlockId(i as u32), label, target));
2839            }
2840        }
2841        out
2842    }
2843
2844    fn finish(self) -> Vec<BasicBlock<'d>> {
2845        self.blocks
2846            .into_iter()
2847            .map(|b| BasicBlock {
2848                bindings: b.bindings,
2849                this_var: b.this_var,
2850                ref_aliases: b.ref_aliases,
2851                stmts: b.stmts,
2852                probes: b.probes,
2853                disable_probes: b.disable_probes,
2854                frame_probes: b.frame_probes,
2855                block_events: b.block_events,
2856                stmt_scopes: b.stmt_scopes,
2857                step_exprs: b.step_exprs,
2858                repeat_init: b.repeat_init.clone(),
2859                repeat_decrement: b.repeat_decrement,
2860                expect_resume: b.expect_resume,
2861                terminator: b.terminator.expect(
2862                    "internal invariant: build_cfg must close every block it allocates \
2863                     before returning Ok",
2864                ),
2865            })
2866            .collect()
2867    }
2868
2869    /// PR-007's single recursive-descent walk. Returns the [`BlockId`] that
2870    /// is "current" for whatever statement follows `stmt` in its enclosing
2871    /// sequence. Every construct reachable in the A2 slice's restricted
2872    /// grammar falls through -- there is no `return`/`disable`/`break`/
2873    /// `continue` meaningfully reachable from a process body here (loops,
2874    /// functions and tasks are out of slice scope), so this never needs to
2875    /// signal "does not fall through".
2876    ///
2877    /// `sv_lang::kinds::StatementKind` is `#[non_exhaustive]` upstream (a
2878    /// `sv-lang-kinds` version bump can add a variant without breaking this
2879    /// crate's build), so a literal Rust `match` over it cannot itself be
2880    /// exhaustive-without-a-wildcard -- the compiler mandates a catch-all
2881    /// arm. Per this workspace's already-established pattern for these
2882    /// generated kind enums (`sv-sim-codegen::module`'s `TimingControlKind`/
2883    /// `StatementKind` handling, `determinism::kind_guard`'s doc comment),
2884    /// this is instead an `if`/`else-if` chain that names every one of
2885    /// today's 32 variants explicitly (`StatementKind::COUNT`, pinned by
2886    /// `determinism::kind_guard::statement_kind_count_matches_pinned_sv_lang_
2887    /// version`), falling through to a named [`LoweringError`] -- never a
2888    /// silent no-op -- only if a future version bump adds a 33rd variant
2889    /// this file has not been reviewed against (which that pinned-count test
2890    /// catches first).
2891    fn walk_stmt(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
2892        // A statically dead `if`/`case` arm slang could not bind demotes the
2893        // enclosing statements to `Invalid` wrappers; the real statement is
2894        // recoverable (see [`unwrap_invalid`]).
2895        let stmt = unwrap_invalid(stmt);
2896        let kind = stmt.kind();
2897
2898        if kind == StatementKind::Invalid {
2899            return Err(LoweringError::malformed(
2900                "Invalid statement node -- slang rejected this statement; an error inside a \
2901                 statically dead `if`/`case` arm (e.g. a reversed range select `v[7:8]` of a \
2902                 descending vector under a false parameter condition) is suppressed by slang \
2903                 but still invalidates the whole enclosing procedure",
2904            ));
2905        }
2906        if kind == StatementKind::List {
2907            return self.walk_seq(live_statements(&stmt), cur);
2908        }
2909        // VPI `cbStmt` (38.36.1, Table 38-6): the statement is about to
2910        // execute. The detached process of `->> @(ctl) e;` runs the same
2911        // statement the issuer already probed. Loops are probed per iteration
2912        // by their own walkers (or by codegen when they stay a leaf).
2913        if is_entry_probe_kind(&stmt) && !self.detached_trigger {
2914            self.probe(cur, stmt);
2915        }
2916        if kind == StatementKind::Block {
2917            return self.walk_block(stmt, cur);
2918        }
2919        if kind == StatementKind::Conditional {
2920            return self.walk_conditional(stmt, cur);
2921        }
2922        if kind == StatementKind::Timed {
2923            return self.walk_timed(stmt, cur);
2924        }
2925        if kind == StatementKind::Wait {
2926            return self.walk_wait(stmt, cur);
2927        }
2928        if kind == StatementKind::WaitFork {
2929            // SC-125: a real suspend -- the scheduler resumes the process once
2930            // every child it forked has finished (`Suspend::WaitFork`).
2931            let resume = self.alloc_block();
2932            self.close(
2933                cur,
2934                Terminator::Suspend {
2935                    point: SuspendPoint::WaitFork,
2936                    resume_blk: resume,
2937                },
2938            );
2939            return Ok(resume);
2940        }
2941        if kind == StatementKind::ExpressionStatement && is_process_by_request_stmt(&stmt) {
2942            // Classes plan Task 13: `p.await();` / `p.suspend();`. The call
2943            // (an ordinary leaf statement) queues the request; the suspend
2944            // after it lets the scheduler apply it -- block in `await`,
2945            // park a self-suspend -- before the next statement runs.
2946            self.push(cur, stmt);
2947            let resume = self.alloc_block();
2948            self.close(
2949                cur,
2950                Terminator::Suspend {
2951                    point: SuspendPoint::ByRequest,
2952                    resume_blk: resume,
2953                },
2954            );
2955            // Classes plan Task 18: a mailbox `get`/`peek` delivers its message
2956            // into a variable, which a process that blocked cannot have done
2957            // when the call returned. The statement runs a second time after
2958            // the resume; the runtime tells the two phases apart (the first one
2959            // of a call that did not block leaves a marker, so the second is a
2960            // no-op: `sv_sim_sim::sched::mailbox`).
2961            if is_mailbox_two_phase_stmt(&stmt) {
2962                self.push(resume, stmt);
2963            }
2964            return Ok(resume);
2965        }
2966        if kind == StatementKind::WaitOrder {
2967            return self.walk_wait_order(stmt, cur);
2968        }
2969        if is_expect(&stmt) && !self.expect_as_leaf {
2970            return self.walk_expect(stmt, cur);
2971        }
2972        if kind == StatementKind::RandCase
2973            && (contains_suspend_trigger(stmt.as_sem_node())
2974                || escapes(
2975                    stmt.as_sem_node(),
2976                    !self.loop_targets.is_empty(),
2977                    !self.return_targets.is_empty(),
2978                ))
2979        {
2980            return self.walk_randcase(stmt, cur);
2981        }
2982        if kind == StatementKind::EventTrigger {
2983            return self.walk_event_trigger(stmt, cur);
2984        }
2985        if kind == StatementKind::ExpressionStatement && async_pla_call(&stmt).is_some() {
2986            return self.walk_async_pla(stmt, cur);
2987        }
2988        if kind == StatementKind::DisableFork {
2989            // SC-125: never suspends -- an ordinary straight-line statement
2990            // that codegen lowers to a `sv_rt_disable_fork` call.
2991            self.push(cur, stmt);
2992            return Ok(cur);
2993        }
2994        if kind == StatementKind::Disable {
2995            // `disable blk;` inside `blk`: leave the block, like `break`
2996            // leaves a loop (LRM 1800-2023 §9.6.2). The target is read from
2997            // the statement's source text (`disable_label`).
2998            // `disable outer.inner;` (9.6.2 hierarchical block name) naming a
2999            // chain of this process's own lexically enclosing blocks is the
3000            // same jump as `disable inner;`.
3001            let mut label_override = None;
3002            if let Some(path) = disable_path(&stmt) {
3003                if let Some(next) = self.walk_hier_disable_of_enclosing(stmt, cur, &path)? {
3004                    return Ok(next);
3005                }
3006                // A block nested in another process's scope, named through
3007                // that scope's blocks (`disable p.inner;`).
3008                label_override = Some(self.hier_peer_leaf(&path)?);
3009            }
3010            let label = match label_override {
3011                Some(l) => l,
3012                None => disable_label(&stmt)?,
3013            };
3014            // `disable f;` inside a branch of fork `f`: ends every branch
3015            // (codegen's runtime call), this one included.
3016            if !self.disable_targets.iter().any(|(n, _, _)| *n == label)
3017                && self.fork_name.as_deref() == Some(label.as_str())
3018            {
3019                self.push(cur, stmt);
3020                self.close(cur, Terminator::End { loop_back: None });
3021                return Ok(self.alloc_block());
3022            }
3023            if let Some(pos) = self
3024                .disable_targets
3025                .iter()
3026                .rposition(|(n, _, _)| *n == label)
3027            {
3028                let (_, exit, frames) = self.disable_targets[pos].clone();
3029                // 16.14.6.4: disabling the process's outermost scope also
3030                // flushes its pending procedural assertion queue; a nested
3031                // block does not. Codegen turns the kept statement into the
3032                // flush.
3033                let flush = pos == 0 && self.outermost.as_deref() == Some(label.as_str());
3034                // A task activation, or a block inside one, can be live in
3035                // other processes too (LRM 9.6.2: every activation ends): the
3036                // kept statement also reaches them at run time.
3037                let broadcast = frames > 0;
3038                // LRM 9.6.2: leaving the block ends the activities enabled
3039                // within it, the fork branches still running among them
3040                // (tagged with the block's name, `disable_names`).
3041                let ends_branches =
3042                    !broadcast && self.forks_in_body() && is_disabled_somewhere(&label);
3043                if flush || broadcast || ends_branches {
3044                    self.push(cur, stmt);
3045                }
3046                if broadcast || ends_branches {
3047                    self.disable_broadcast.push((label.clone(), false));
3048                }
3049                self.disable_probe(cur, stmt);
3050                // The inlined tasks the jump leaves end here, innermost first.
3051                let left: Vec<_> = self.frame_stack[frames..].iter().rev().cloned().collect();
3052                for (task, vars) in left {
3053                    self.frame_probe(cur, FrameProbe::Exit { task, vars });
3054                }
3055                self.left_block_events(cur, pos);
3056                self.jump(cur, exit);
3057                return Ok(self.alloc_block());
3058            }
3059            // `disable a1;` of a procedural concurrent assertion's label
3060            // (16.14.6.4) clears that assertion's pending instances and goes
3061            // on: the statement is kept for codegen.
3062            if self.assertion_labels.contains(&label) {
3063                self.push(cur, stmt);
3064                return Ok(cur);
3065            }
3066            // `disable a1;` of a deferred immediate assertion's label (16.4.4):
3067            // cancels its pending reports and goes on; kept for codegen.
3068            if self.deferred_labels.contains(&label) {
3069                self.push(cur, stmt);
3070                return Ok(cur);
3071            }
3072            // `disable L;` of a plain immediate assertion's label from outside
3073            // its statement: the statement has finished, nothing to end.
3074            if self.immediate_labels.contains(&label) {
3075                return Ok(cur);
3076            }
3077            // `disable b2;` of another process's outermost block (16.14.6.4,
3078            // the `b3` example): the other process cannot be inside a block
3079            // without a suspend point, so the disable's whole effect is that
3080            // process's queue flush.
3081            if let Some(can_suspend) = self
3082                .peers
3083                .iter()
3084                .find(|p| p.name == label)
3085                .map(|p| p.can_suspend)
3086            {
3087                self.push(cur, stmt);
3088                // A block with a suspend point may have its process inside it:
3089                // the disable then resumes that process after the block
3090                // (`sv_rt_disable_named`, `ProcessCfg::disable_scopes`).
3091                if can_suspend {
3092                    self.disable_broadcast.push((label, true));
3093                }
3094                return Ok(cur);
3095            }
3096            // `disable u1.blk;` of a block of a child instance: every process
3097            // of that instance suspended inside the block resumes after it.
3098            if label.contains('.') {
3099                self.push(cur, stmt);
3100                self.disable_broadcast.push((label, true));
3101                return Ok(cur);
3102            }
3103            // `disable t;` of a task of the design that this process is not in:
3104            // it ends every activation of `t` in the other processes (LRM
3105            // 9.6.2) and the process goes on.
3106            if is_task_name(&label) {
3107                self.push(cur, stmt);
3108                self.disable_broadcast.push((label, true));
3109                return Ok(cur);
3110            }
3111            // LRM 9.6.2: `label` is a fork branch's own named outermost
3112            // block reachable from here -- the fork's parent (continuing
3113            // past it), a sibling branch, or a descendant of an ancestor
3114            // fork -- but not through a same-process lexically enclosing
3115            // block or this CFG's own `fork_name` (both already checked
3116            // above). Kept as an ordinary statement: codegen (`sv-sim-
3117            // codegen::process_lower`) tells this case apart from the ones
3118            // above by `ProcessLoweringPlan::fork_branch_names` and lowers
3119            // it to a runtime scan/kill of every live, so-tagged process
3120            // reachable from the caller's own fork family
3121            // (`sv_sim_sim::sched::SchedulerOps::disable_named`) -- never a
3122            // silent no-op when nothing is currently live (the same
3123            // "already finished, nothing to do" convention `disable fork;`
3124            // itself uses, LRM 9.6.3).
3125            if self.fork_branch_names.iter().any(|n| n == &label) {
3126                self.push(cur, stmt);
3127                return Ok(cur);
3128            }
3129            // LRM 9.6.2: `label` is a named block of the forking process
3130            // that encloses the fork this branch belongs to. The disable
3131            // ends all activity in the block: the siblings, this branch, and
3132            // the parent's wait at the join, which resumes after the block
3133            // (`sv_rt_disable_named`, the parent's `disable_scopes`). This
3134            // branch ends here, like `disable <fork name>;` above.
3135            if self.ancestor_blocks.iter().any(|n| n == &label) {
3136                self.push(cur, stmt);
3137                self.disable_broadcast.push((label, true));
3138                self.close(cur, Terminator::End { loop_back: None });
3139                return Ok(self.alloc_block());
3140            }
3141            return Err(LoweringError::unsupported(format!(
3142                "disable {label}; where `{label}` is not a named block or task enclosing it \
3143                 in the same process, a procedural assertion label of it, a uniquely named \
3144                 sequential block of another process or a task of the design (not a block \
3145                 inside a fork branch of another process, nor a name shared by two blocks), \
3146                 or a fork branch's own name reachable from here"
3147            )));
3148        }
3149        if kind == StatementKind::ForLoop {
3150            return self.walk_for_loop(stmt, cur);
3151        }
3152        if kind == StatementKind::WhileLoop {
3153            return self.walk_while_loop(stmt, cur);
3154        }
3155        if kind == StatementKind::DoWhileLoop {
3156            return self.walk_do_while_loop(stmt, cur);
3157        }
3158        if kind == StatementKind::ForeverLoop {
3159            return self.walk_forever_loop(stmt, cur);
3160        }
3161        if kind == StatementKind::ForeachLoop {
3162            return self.walk_foreach_loop(stmt, cur);
3163        }
3164        // R-run Task 8 (PR-045/PR-006): `repeat(N) body;` gets a real CFG
3165        // `Loop` shape ONLY when `body` contains a suspend point somewhere
3166        // in its own subtree -- a suspend inside a `repeat` needs its own
3167        // iteration counter to survive the resulting suspend/resume call
3168        // boundary (see `RepeatCounterId`'s own doc comment), which only a
3169        // real CFG split can provide. The suspend-free case stays exactly
3170        // the pre-existing opaque leaf `is_ordinary_statement_kind` already
3171        // routes to `stmt_shared::lower_repeat_loop` (R-run Task 4) --
3172        // falling through to that check below, unchanged, whenever this
3173        // condition is false, deliberately preserving
3174        // `repeat_loop_control_flow_shape_matches_between_function_and_
3175        // process_paths`'s own "a suspend-free repeat loop stays an
3176        // ordinary, un-decomposed leaf statement" invariant
3177        // (`shared_stmt_lowering.rs`).
3178        if kind == StatementKind::RepeatLoop
3179            && (contains_suspend_trigger(stmt.as_sem_node())
3180                || escapes(stmt.as_sem_node(), false, !self.return_targets.is_empty()))
3181        {
3182            return self.walk_repeat_loop(stmt, cur);
3183        }
3184
3185        // R-run Task 1: `$finish`/`$stop`/`$fatal` are a segment-closing
3186        // point, same category as a suspend -- LRM 1800-2023 §20.2/§20.10:
3187        // once one of these executes, none of the calling process's own
3188        // textually-following statements may ever run (not just the next
3189        // one in a flat statement list -- also not one nested arbitrarily
3190        // deep inside an `if`/`case`/loop branch this "ordinary straight-
3191        // line" arm below would otherwise happily keep pushing into the
3192        // SAME still-open block). This is checked before the generic
3193        // `is_ordinary_statement_kind` dispatch below (an `ExpressionStatement`
3194        // wrapping one of these calls is itself an ordinary-kind statement,
3195        // so without this earlier check it would fall straight into that
3196        // arm and keep accumulating every statement that follows it into
3197        // `cur`).
3198        //
3199        // Reuses the existing `Terminator::End { loop_back: None }` shape
3200        // (PR-069's "no new enum catch-all" extension policy) rather than a
3201        // new `Terminator::Halt`/`Terminator::Finish` variant: this closes
3202        // `cur` as a real process-kernel exit (`SegTerminator::End{loop_back:
3203        // None}` -> the compiled kernel returns `SEG_FINISHED` for this
3204        // call, per `lower_terminator`'s existing 1:1 mapping), unconditionally
3205        // `loop_back: None` even inside an `always`/`always_ff` process
3206        // (`ProcessLoopKind::Looping` would otherwise close the process's
3207        // FINAL block with `Some(entry)`) -- once `$finish`/`$stop`/`$fatal`
3208        // runs, the calling process itself must never be re-entered again,
3209        // regardless of what kind of process it is; the *global* simulation
3210        // shutdown that a bare `$finish`/`$stop` also requests is a
3211        // scheduler-level concern (`SchedulerOps::request_finish`,
3212        // `sv_rt_finish`'s own doc comment) this file has no visibility
3213        // into and does not need to model.
3214        //
3215        // Any statement textually following the call (whether later in this
3216        // same flat list, as `walk_seq`'s next loop iteration, or -- via
3217        // this function's own caller chain, e.g. `walk_conditional` closing
3218        // `then_exit`/`else_exit` with `Terminator::Fallthrough(join)` --
3219        // structurally nested deeper) is walked into a FRESH block instead
3220        // of `cur`: `cur` is already closed, so it can never be reached
3221        // again, and neither can anything later routed into the fresh
3222        // block, since nothing in this builder ever branches to a block
3223        // this function itself did not just hand back as "the current
3224        // exit" to some earlier, still-open predecessor -- there is none
3225        // here. The fresh block still gets a real terminator from whatever
3226        // caller eventually closes it (a plain, harmless `Fallthrough`/
3227        // `End` on dead code), satisfying `finish()`'s "every block must be
3228        // closed" invariant without special-casing unreachable blocks.
3229        // A call to a task whose body can suspend is inlined here, so its
3230        // timing controls become real suspend points of this process.
3231        if kind == StatementKind::ExpressionStatement
3232            && let Some(e) = stmt.expr()
3233            && crate::netlist::itf::is_vif_extern_call(&e)
3234        {
3235            return Err(LoweringError::unsupported(
3236                crate::netlist::itf::vif_extern_message(&e),
3237            ));
3238        }
3239        if kind == StatementKind::ExpressionStatement
3240            && let Some(e) = stmt.expr()
3241            && let Some(targets) = crate::netlist::itf::call_extern_targets(&e)
3242            && let Some(next) = self.walk_extern_call(stmt, e, &targets, cur)?
3243        {
3244            return Ok(next);
3245        }
3246        if kind == StatementKind::ExpressionStatement
3247            && let Some(e) = stmt.expr()
3248            && let Some(callee) = suspending_task_callee(&e)
3249        {
3250            return self.inline_task_call(e, callee, cur);
3251        }
3252        if matches!(kind, StatementKind::Break | StatementKind::Continue) {
3253            let &(cont, brk) = self
3254                .loop_targets
3255                .last()
3256                .ok_or_else(|| LoweringError::malformed(format!("{kind:?} outside any loop")))?;
3257            let target = if kind == StatementKind::Break {
3258                brk
3259            } else {
3260                cont
3261            };
3262            self.jump(cur, target);
3263            return Ok(self.alloc_block());
3264        }
3265        if (kind == StatementKind::Case || kind == StatementKind::PatternCase)
3266            && (contains_suspend_trigger(stmt.as_sem_node())
3267                || constant_case_live_arm(&stmt).is_some()
3268                || escapes(
3269                    stmt.as_sem_node(),
3270                    !self.loop_targets.is_empty(),
3271                    !self.return_targets.is_empty(),
3272                ))
3273        {
3274            return self.walk_case(stmt, cur);
3275        }
3276        if kind == StatementKind::ImmediateAssertion
3277            && let Some(actions) = suspending_assertion_actions(&stmt)
3278        {
3279            return self.walk_immediate_assertion(stmt, actions, cur);
3280        }
3281        if kind == StatementKind::ExpressionStatement
3282            && let Some(e) = stmt.expr()
3283            && e.kind() == ExpressionKind::Assignment
3284            && e.is_nonblocking() != Some(true)
3285            && e.assignment_timing().is_some()
3286        {
3287            return self.walk_intra_assignment_timing(e, cur);
3288        }
3289        // `q <= @(e) v;` / `q <= repeat (n) @(e) v;` (LRM 10.4.2). A plain
3290        // delay stays a statement: the staged write carries it.
3291        if kind == StatementKind::ExpressionStatement
3292            && let Some(e) = stmt.expr()
3293            && e.kind() == ExpressionKind::Assignment
3294            && e.is_nonblocking() == Some(true)
3295            && e.assignment_timing()
3296                .and_then(|n| n.as_timing_control())
3297                .is_some_and(|tc| tc.kind() != TimingControlKind::Delay)
3298        {
3299            return self.walk_nba_event(stmt, e, cur);
3300        }
3301        if kind == StatementKind::Return
3302            && let Some(&target) = self.return_targets.last()
3303        {
3304            // `return;` inside an inlined task: jump past the task body.
3305            // `return expr;` of an inlined function first assigns its result
3306            // variable (in a block of its own: bindings run on block entry).
3307            let mut cur = cur;
3308            if self.ret_var_stack.len() == self.return_targets.len()
3309                && let Some(Some(rv)) = self.ret_var_stack.last().copied()
3310                && let Some(value) = stmt.expr()
3311            {
3312                if let Some(next) = self.hoist_suspending_calls_in(value.as_sem_node(), cur)? {
3313                    cur = next;
3314                }
3315                let set = self.alloc_block();
3316                self.close(cur, Terminator::Fallthrough(set));
3317                self.blocks[set.0 as usize].bindings.push(ArgBinding::In {
3318                    formal: rv,
3319                    actual: value,
3320                });
3321                cur = set;
3322            }
3323            self.jump(cur, target);
3324            return Ok(self.alloc_block());
3325        }
3326        if kind == StatementKind::ExpressionStatement && is_finish_or_stop_call(&stmt) {
3327            self.push(cur, stmt);
3328            self.close(cur, Terminator::End { loop_back: None });
3329            return Ok(self.alloc_block());
3330        }
3331
3332        // PR-006's own acceptance text: "any other kind is ordinary
3333        // straight-line code". Kept as one opaque `Statement` leaf unless it
3334        // secretly contains a suspend-triggering construct somewhere in its
3335        // subtree, in which case there is no `Terminator::Loop`/`Case` to
3336        // represent the needed split (see this file's module doc comment's
3337        // extension policy) -- a named `LoweringError`, never a
3338        // silently-wrong CFG.
3339        if is_ordinary_statement_kind(kind) {
3340            if kind == StatementKind::ExpressionStatement
3341                && contains_suspend_trigger(stmt.as_sem_node())
3342                && let Some(next) = self.hoist_suspending_function_calls(stmt, cur)?
3343            {
3344                return Ok(next);
3345            }
3346            return if contains_suspend_trigger(stmt.as_sem_node())
3347                && !(is_expect(&stmt) && self.expect_as_leaf)
3348            {
3349                Err(LoweringError::unsupported(format!(
3350                    "suspend point nested inside a {kind:?} statement (no CFG terminator \
3351                     for this construct is implemented in this task, PR-069)"
3352                )))
3353            } else {
3354                self.push(cur, stmt);
3355                if kind == StatementKind::ExpressionStatement
3356                    && has_function_disables()
3357                    && contains_subroutine_call(stmt.as_sem_node())
3358                {
3359                    return Ok(self.fn_disable_check(cur));
3360                }
3361                Ok(cur)
3362            };
3363        }
3364
3365        // Unreachable as long as `determinism::kind_guard`'s pinned
3366        // `StatementKind::COUNT == 32` still holds -- every one of today's 32
3367        // variants is named above or in `is_ordinary_statement_kind`. A
3368        // `sv-lang-kinds` bump that adds a 33rd fails that pinned-count test
3369        // loudly; until this file is reviewed and updated, reaching here is a
3370        // named error, never a silent fallthrough.
3371        Err(LoweringError::malformed(format!(
3372            "unrecognized StatementKind {kind:?} (sv-lang-kinds version drift -- see \
3373             determinism::kind_guard's StatementKind pin)"
3374        )))
3375    }
3376
3377    /// `expect (property) pass else fail;` (LRM 1800-2023 §16.17): blocks the
3378    /// process until the assertion engine has decided the property.
3379    ///
3380    /// The statement appears twice, as the assertion's two halves. In the block
3381    /// it is reached in, it queues the evaluation (codegen's
3382    /// `sv_rt_proc_assert`); the process then parks with no sensitivity at all
3383    /// (an empty [`SuspendPoint::EdgeList`]) until the engine, in the Observed
3384    /// region of the step it decides in, wakes it. In the block it resumes into
3385    /// ([`BasicBlock::expect_resume`]) it runs the pass or `else` statement, and
3386    /// whatever follows the `expect` goes on from there.
3387    fn walk_expect(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
3388        let blocking = [stmt.assertion_if_true(), stmt.assertion_if_false()]
3389            .into_iter()
3390            .flatten()
3391            .any(|a| contains_suspend_trigger(a.as_sem_node()));
3392        if blocking {
3393            return self.walk_expect_blocking(stmt, cur);
3394        }
3395        self.push(cur, stmt);
3396        let resume = self.alloc_block();
3397        self.blocks[resume.0 as usize].expect_resume = true;
3398        self.close(
3399            cur,
3400            Terminator::Suspend {
3401                point: SuspendPoint::EdgeList(Vec::new(), Vec::new()),
3402                resume_blk: resume,
3403            },
3404        );
3405        self.push(resume, stmt);
3406        Ok(resume)
3407    }
3408
3409    /// An `expect` whose pass or `else` action blocks (a delay, an event
3410    /// control, a wait, another `expect`; LRM 1800-2023 16.17 allows any
3411    /// statement there). The action cannot run inline in the resume
3412    /// segment, so the resume block switches on the outcome the engine woke
3413    /// the process with (`sv_rt_fired_term`: 0 pass, 1 fail, 2 -- actions
3414    /// switched off by `$assertcontrol`, 20.11 -- neither) and walks each
3415    /// action as ordinary blocks. A fail with no `else` reports the `$error`
3416    /// through a zero-arm switch, as `wait_order` does.
3417    fn walk_expect_blocking(
3418        &mut self,
3419        stmt: Statement<'d>,
3420        cur: BlockId,
3421    ) -> Result<BlockId, LoweringError> {
3422        self.push(cur, stmt);
3423        let resume = self.alloc_block();
3424        self.close(
3425            cur,
3426            Terminator::Suspend {
3427                point: SuspendPoint::EdgeList(Vec::new(), Vec::new()),
3428                resume_blk: resume,
3429            },
3430        );
3431        let join = self.alloc_block();
3432        let pass = self.alloc_block();
3433        let fail = self.alloc_block();
3434        let pass_exit = match stmt.assertion_if_true() {
3435            Some(s) => self.walk_stmt(s, pass)?,
3436            None => pass,
3437        };
3438        self.close(pass_exit, Terminator::Fallthrough(join));
3439        let fail_exit = match stmt.assertion_if_false() {
3440            Some(s) => self.walk_stmt(s, fail)?,
3441            None => {
3442                self.close(
3443                    fail,
3444                    Terminator::Switch {
3445                        on: SwitchOn::FiredTerm,
3446                        arms: Vec::new(),
3447                        default_blk: join,
3448                        report: Some(stmt.source_range().start),
3449                    },
3450                );
3451                self.alloc_block()
3452            }
3453        };
3454        self.close(fail_exit, Terminator::Fallthrough(join));
3455        self.close(
3456            resume,
3457            Terminator::Switch {
3458                on: SwitchOn::FiredTerm,
3459                arms: vec![pass, fail, join],
3460                default_blk: join,
3461                report: None,
3462            },
3463        );
3464        Ok(join)
3465    }
3466
3467    /// A `randcase` whose items need their own blocks (one suspends, or
3468    /// escapes an enclosing loop or task): [`SwitchOn::RandCase`] picks one.
3469    fn walk_randcase(
3470        &mut self,
3471        stmt: Statement<'d>,
3472        cur: BlockId,
3473    ) -> Result<BlockId, LoweringError> {
3474        let join = self.alloc_block();
3475        let mut arms = Vec::new();
3476        for item in stmt.randcase_items() {
3477            let arm = self.alloc_block();
3478            arms.push(arm);
3479            let exit = self.walk_stmt(item.stmt(), arm)?;
3480            self.close(exit, Terminator::Fallthrough(join));
3481        }
3482        self.close(
3483            cur,
3484            Terminator::Switch {
3485                on: SwitchOn::RandCase(stmt),
3486                arms,
3487                default_blk: join,
3488                report: None,
3489            },
3490        );
3491        Ok(join)
3492    }
3493
3494    /// `wait_order(e1, ..., en) pass else fail;` (LRM 1800-2023 §15.5.4):
3495    /// step `i` waits on `e_i` .. `e_n`, and goes on only if `e_i` is the one
3496    /// that fired (the wait's term 0, [`SwitchOn::FiredTerm`]) -- any other
3497    /// fires out of order and fails. After `e_n`, the pass action; a
3498    /// failure without an `else` reports `$error`.
3499    fn walk_wait_order(
3500        &mut self,
3501        stmt: Statement<'d>,
3502        cur: BlockId,
3503    ) -> Result<BlockId, LoweringError> {
3504        let events = stmt.wait_order_events();
3505        if events.is_empty() {
3506            return Err(LoweringError::malformed("wait_order with no events"));
3507        }
3508        let pass = self.alloc_block();
3509        let fail = self.alloc_block();
3510        let join = self.alloc_block();
3511        let if_false = stmt.wait_order_if_false();
3512        let report = if_false.is_none().then(|| stmt.source_range().start);
3513        let mut at = cur;
3514        for i in 0..events.len() {
3515            let resume = self.alloc_block();
3516            self.close(
3517                at,
3518                Terminator::Suspend {
3519                    point: SuspendPoint::EdgeList(
3520                        events[i..]
3521                            .iter()
3522                            .map(|e| (*e, sv_lang::EdgeKind::None))
3523                            .collect(),
3524                        vec![Vec::new(); events.len() - i],
3525                    ),
3526                    resume_blk: resume,
3527                },
3528            );
3529            let next = if i + 1 == events.len() {
3530                pass
3531            } else {
3532                self.alloc_block()
3533            };
3534            self.close(
3535                resume,
3536                Terminator::Switch {
3537                    on: SwitchOn::FiredTerm,
3538                    arms: vec![next],
3539                    default_blk: fail,
3540                    report,
3541                },
3542            );
3543            at = next;
3544        }
3545        let pass_exit = match stmt.wait_order_if_true() {
3546            Some(s) => self.walk_stmt(s, pass)?,
3547            None => pass,
3548        };
3549        self.close(pass_exit, Terminator::Fallthrough(join));
3550        let fail_exit = match if_false {
3551            Some(s) => self.walk_stmt(s, fail)?,
3552            None => fail,
3553        };
3554        self.close(fail_exit, Terminator::Fallthrough(join));
3555        Ok(join)
3556    }
3557
3558    /// Inlines `call` (to `task`, whose body can suspend) at `cur`: a fresh
3559    /// entry block binds the formals ([`ArgBinding::In`]/[`ArgBinding::
3560    /// Init`]) and initializes the task's locals, the body is walked in
3561    /// place (a `return;` jumps to the continuation), and the continuation
3562    /// block starts with the output copy-backs ([`ArgBinding::Out`]).
3563    fn inline_task_call(
3564        &mut self,
3565        call: Expression<'d>,
3566        task: Symbol<'d>,
3567        cur: BlockId,
3568    ) -> Result<BlockId, LoweringError> {
3569        let result_temp = self.pending_result_temp.take();
3570        // Through a virtual interface (`vif.t(...)`): the task of the
3571        // instance it selects -- one inlined copy per candidate instance
3572        // (`netlist::vif`), picked by its id; `null` runs nothing.
3573        if let Some((vif, targets)) = crate::netlist::vif::call_targets(&call) {
3574            let actuals: Vec<Expression<'d>> = call.operands()[1..].to_vec();
3575            let join = self.alloc_block();
3576            let mut arms = Vec::new();
3577            for target in targets {
3578                let arm = self.alloc_block();
3579                arms.push(arm);
3580                match target {
3581                    Some(t) => {
3582                        let exit = self.inline_task_body(&actuals, t, arm, None)?;
3583                        self.park_call_result(exit, result_temp, t);
3584                        self.close(exit, Terminator::Fallthrough(join));
3585                    }
3586                    None => self.close(arm, Terminator::Fallthrough(join)),
3587                }
3588            }
3589            self.close(
3590                cur,
3591                Terminator::Switch {
3592                    on: SwitchOn::VifId(vif),
3593                    arms,
3594                    default_blk: join,
3595                    report: None,
3596                },
3597            );
3598            return Ok(join);
3599        }
3600        // A class-method task (LRM 8.6): its `this` is bound to the receiver
3601        // (`obj.run()`), or for an implicit-`this` call (`run()` inside
3602        // another method) to the caller's current object, and kept in a frame
3603        // local across the suspend points.
3604        self.call_anchor = Some(call);
3605        let receiver = call.call_this_class();
3606        let mut actuals = call.operands();
3607        if receiver.is_some() && !actuals.is_empty() {
3608            actuals.remove(0);
3609        }
3610        // A `pure virtual` task has no body and so no `this` of its own; its
3611        // calls still dispatch over the implementations below (LRM 8.21).
3612        let pure = task.subroutine_flags().contains(sv_lang::MethodFlags::PURE);
3613        let this_var = task.subroutine_this_var();
3614        if this_var.is_none() && !pure {
3615            let exit = self.inline_task_body(&actuals, task, cur, None)?;
3616            self.park_call_result(exit, result_temp, task);
3617            return Ok(exit);
3618        }
3619        // A virtual task is bound by the object's class at run time (LRM
3620        // 8.20): one inlined override per implementation the possible dynamic
3621        // classes resolve it to, picked by the object's class. A call through
3622        // `super.` is not virtual.
3623        if !(receiver.is_none() && is_explicit_super(call))
3624            && (task
3625                .subroutine_flags()
3626                .contains(sv_lang::MethodFlags::VIRTUAL)
3627                || task.subroutine_override().is_some())
3628        {
3629            let impls = self
3630                .virtual_tasks
3631                .as_ref()
3632                .map(|v| v.implementations(&task))
3633                .unwrap_or_default();
3634            // No class the design creates implements it: no object can
3635            // reach the call, so it can only run on a null handle (an
3636            // error at run time, as for any call through `null`). E.g. UVM's
3637            // `uvm_sequencer_base::start_phase_sequence` in a bench with no
3638            // sequencer.
3639            let join = self.alloc_block();
3640            let mut arms = Vec::new();
3641            let mut ids = Vec::new();
3642            for (class_ids, imp) in impls {
3643                let this_var = imp.subroutine_this_var().ok_or_else(|| {
3644                    LoweringError::malformed(format!(
3645                        "override of virtual task `{}` has no `this`",
3646                        task.name()
3647                    ))
3648                })?;
3649                let arm = self.alloc_block();
3650                arms.push(arm);
3651                ids.push(class_ids);
3652                let exit = self.inline_task_body(&actuals, imp, arm, Some((this_var, receiver)))?;
3653                self.park_call_result(exit, result_temp, imp);
3654                self.close(exit, Terminator::Fallthrough(join));
3655            }
3656            self.close(
3657                cur,
3658                Terminator::Switch {
3659                    on: SwitchOn::ObjClass { receiver, ids },
3660                    arms,
3661                    default_blk: join,
3662                    // A null (or dead) handle runs no override: an error.
3663                    report: Some(call.source_range().start),
3664                },
3665            );
3666            return Ok(join);
3667        }
3668        let this_var = this_var.ok_or_else(|| {
3669            LoweringError::malformed(format!("method task `{}` has no `this`", task.name()))
3670        })?;
3671        let exit = self.inline_task_body(&actuals, task, cur, Some((this_var, receiver)))?;
3672        self.park_call_result(exit, result_temp, task);
3673        Ok(exit)
3674    }
3675
3676    /// At the end of an inlined function `imp` (block `exit`): copies its
3677    /// result variable into the call's capture temporary, when it has one.
3678    fn park_call_result(
3679        &mut self,
3680        exit: BlockId,
3681        result_temp: Option<(u32, Expression<'d>)>,
3682        imp: Symbol<'d>,
3683    ) {
3684        if let Some((temp, _)) = result_temp
3685            && let Some(rv) = imp.subroutine_return_val_var()
3686        {
3687            self.blocks[exit.0 as usize]
3688                .bindings
3689                .push(ArgBinding::ToCaptured { temp, formal: rv });
3690        }
3691    }
3692
3693    /// The first call of a task that calls itself (directly or through other
3694    /// tasks) and contains timing controls (LRM 1800-2023 §13.4, §13.4.1):
3695    /// inlining cannot bound the depth, so each activation is a forked child
3696    /// of its caller that the caller joins. The caller evaluates the
3697    /// arguments now ([`ArgBinding::Capture`]), the child copies them into
3698    /// its own formals ([`ArgBinding::FromCaptured`]) and runs the body in a
3699    /// frame of its own (automatic variables, §6.21); a call inside the body
3700    /// forks the same kernel again ([`Self::recursive_self_call`]).
3701    ///
3702    /// `output` and `inout` formals (LRM 1800-2023 13.5) are copied out when
3703    /// the activation ends: parked in a temporary ([`ArgBinding::ToCaptured`]),
3704    /// handed to the spawner's own temporary ([`InheritedSlot::Return`]) and
3705    /// assigned to the actual after the join ([`ArgBinding::AssignCaptured`]).
3706    /// They are limited to what a capture temporary holds (a packed value of
3707    /// at most 128 bits).
3708    ///
3709    /// A class method (LRM 1800-2023 8.11) also takes its `this` as capture 0
3710    /// (an implicit receiver by [`ArgBinding::CaptureThis`]). A recursive
3711    /// void function that spawns processes is handled the same way; its
3712    /// `join_none` children may then start earlier than in a call that never
3713    /// yields (the caller waits at each recursive join).
3714    ///
3715    /// Deviation: the activation is a fork child, so `disable fork` and
3716    /// `wait fork` of the caller see it; only tasks with automatic lifetime
3717    /// are supported (`ref` / static formals and a `disable` of the task fail
3718    /// by name).
3719    fn inline_recursive_task(
3720        &mut self,
3721        actuals: &[Expression<'d>],
3722        task: Symbol<'d>,
3723        cur: BlockId,
3724        this: Option<(Symbol<'d>, Option<Expression<'d>>)>,
3725    ) -> Result<BlockId, LoweringError> {
3726        let refuse = |why: &str| {
3727            LoweringError::unsupported(format!(
3728                "recursive call to task `{}`, which contains timing controls: {why} \
3729                 (LRM 1800-2023 13.4.1)",
3730                task.name()
3731            ))
3732        };
3733        let body = proc_body(&task).ok_or_else(|| {
3734            LoweringError::malformed(format!("task `{}` has no body", task.name()))
3735        })?;
3736        let mut formals: Vec<Symbol<'d>> = task.subroutine_arguments().collect();
3737        if formals.len() != actuals.len() {
3738            return Err(LoweringError::malformed(format!(
3739                "call to task `{}` has {} arguments for {} formals",
3740                task.name(),
3741                actuals.len(),
3742                formals.len()
3743            )));
3744        }
3745        if task.subroutine_kind() == Some(sv_lang::SubroutineKind::Function)
3746            && task.subroutine_return_type().is_some_and(|t| !t.is_void())
3747        {
3748            return Err(refuse("a value-returning function is not supported"));
3749        }
3750        for f in &formals {
3751            if crate::netlist::is_static_procedural_var(f) {
3752                return Err(refuse("a recursive task must be `automatic`"));
3753            }
3754        }
3755        // slang binds an output/inout actual as `actual = <formal>`: the
3756        // actual is its target.
3757        let mut actuals: Vec<Expression<'d>> = actuals
3758            .iter()
3759            .zip(&formals)
3760            .map(|(&a, f)| {
3761                if f.formal_argument_direction() == sv_lang::ArgumentDirection::In {
3762                    a
3763                } else {
3764                    call_actual_lvalue(a)
3765                }
3766            })
3767            .collect();
3768        // A class method: its `this` is formal 0, an input handle.
3769        let has_this = this.is_some();
3770        let mut implicit_this = false;
3771        if let Some((this_var, recv)) = this {
3772            let anchor = self.call_anchor.ok_or_else(|| {
3773                LoweringError::malformed("recursive method call without a call expression")
3774            })?;
3775            implicit_this = recv.is_none();
3776            formals.insert(0, this_var);
3777            actuals.insert(0, recv.unwrap_or(anchor));
3778        }
3779        let n = formals.len() as u32;
3780        let actuals = &actuals[..];
3781        // A `ref` formal bound to static storage (a variable or a net, directly or
3782        // through an enclosing call's aliased `ref`) is that storage in every
3783        // activation (LRM 1800-2023 13.5.2): no capture, no copy-back. Every
3784        // recursive call must bind it to the same storage
3785        // ([`Self::recursive_self_call`]); anything else is refused by name.
3786        let mut ref_alias: Vec<Option<Symbol<'d>>> = vec![None; formals.len()];
3787        for (i, f) in formals.iter().enumerate() {
3788            if f.formal_argument_direction() != sv_lang::ArgumentDirection::Ref {
3789                continue;
3790            }
3791            let is_event = f
3792                .value_type()
3793                .as_ref()
3794                .is_some_and(|t| crate::state::event_pool::event_count(t).is_some());
3795            match self.ref_storage(actuals[i]) {
3796                Some(t) if !is_event => ref_alias[i] = Some(t),
3797                _ => {
3798                    return Err(refuse(
3799                        "a `ref` argument must name a static variable or net (or an \
3800                         enclosing task's aliased `ref` formal)",
3801                    ));
3802                }
3803            }
3804        }
3805        let is_in = |i: usize| {
3806            (has_this && i == 0)
3807                || formals[i].formal_argument_direction() == sv_lang::ArgumentDirection::In
3808                || ref_alias[i].is_some()
3809        };
3810        let outs: Vec<usize> = (0..formals.len()).filter(|&i| !is_in(i)).collect();
3811        let groups = if outs.is_empty() { 2 } else { 4 };
3812
3813        // The branch: its own builder. Capture temporaries `0..n` hold the
3814        // incoming arguments, `n..2n` those of a recursive call; with
3815        // `output`/`inout` formals `2n..3n` hold this activation's results
3816        // and `3n..4n` those of a recursive call.
3817        // The same task activated from the same context builds the same
3818        // Root: build it once per process ([`RootKey`]) and the lowering
3819        // compiles it once ([`ProcessCfg::share_id`]); only the call site's
3820        // bindings below are per site. Not under a clock inference.
3821        let key = (self.global_clock.is_none() && self.checker_clock.is_none()).then(|| {
3822            MemoKey::Root(RootKey {
3823                task: task.uid(),
3824                this_var: this.map(|(t, _)| t.uid()),
3825                default_clocking: self.default_clocking.map(|c| c.uid()),
3826                rec_outer: self.rec_outer.clone(),
3827                chain: self
3828                    .rec_up
3829                    .iter()
3830                    .chain(self.rec.iter())
3831                    .map(|r| r.share)
3832                    .collect(),
3833                ref_aliases: self
3834                    .ref_stack
3835                    .iter()
3836                    .map(|(f, t)| (f.uid(), t.uid()))
3837                    .chain(
3838                        formals
3839                            .iter()
3840                            .zip(&ref_alias)
3841                            .filter_map(|(f, t)| t.map(|t| (f.uid(), t.uid()))),
3842                    )
3843                    .collect(),
3844                labels: self.fork_branch_names.clone(),
3845                ancestors: self.ancestor_blocks.clone(),
3846            })
3847        });
3848        let cached = key
3849            .as_ref()
3850            .and_then(|k| self.fork_memo.borrow().get(k).cloned());
3851        let mut branch = if let Some(hit) = cached {
3852            hit
3853        } else {
3854            let share = NEXT_SHARE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3855            let mut b = CfgBuilder::new();
3856            b.fork_memo = self.fork_memo.clone();
3857            b.global_clock = self.global_clock;
3858            b.virtual_tasks = self.virtual_tasks.clone();
3859            b.fork_branch_names = self.fork_branch_names.clone();
3860            b.ancestor_blocks = self.ancestor_blocks.clone();
3861            b.default_clocking = self.default_clocking;
3862            b.checker_clock = self.checker_clock;
3863            b.rec_outer = self.rec_outer.clone();
3864            b.rec_outer.push(task.uid());
3865            b.rec_up = self.rec_up.clone();
3866            b.rec_up.extend(self.rec.clone());
3867            b.rec = Some(RecursiveTask {
3868                task: task.uid(),
3869                share,
3870                proto: actuals
3871                    .iter()
3872                    .copied()
3873                    .zip(formals.iter().copied())
3874                    .collect(),
3875                n,
3876                groups,
3877                outs: (0..formals.len()).map(|i| !is_in(i)).collect(),
3878                refs: ref_alias.clone(),
3879                has_this,
3880                body,
3881            });
3882            b.ref_stack = self.ref_stack.clone();
3883            for (f, t) in formals.iter().zip(&ref_alias) {
3884                if let Some(t) = t {
3885                    b.ref_stack.push((*f, *t));
3886                    b.ref_candidates.push((*f, Some(*t)));
3887                }
3888            }
3889            if let Some((this_var, _)) = this {
3890                b.this_stack.push(this_var);
3891            }
3892            for _ in 0..groups {
3893                for (f, &a) in formals.iter().zip(actuals) {
3894                    b.set_capture_type(a, *f);
3895                }
3896            }
3897            let entry = b.alloc_block();
3898            for (i, f) in formals.iter().enumerate() {
3899                // An `output` formal starts out as its type's default.
3900                if ref_alias[i].is_some() {
3901                    continue;
3902                }
3903                b.blocks[entry.0 as usize].bindings.push(
3904                    if !is_in(i) && f.formal_argument_direction() == sv_lang::ArgumentDirection::Out
3905                    {
3906                        ArgBinding::Init { var: *f }
3907                    } else {
3908                        ArgBinding::FromCaptured {
3909                            temp: i as u32,
3910                            formal: *f,
3911                        }
3912                    },
3913                );
3914                b.inlined_locals.push(*f);
3915            }
3916            for local in task_locals(&task) {
3917                // A method's `this` is bound above (formal 0).
3918                if this.is_some_and(|(t, _)| t.uid() == local.uid()) {
3919                    continue;
3920                }
3921                if crate::netlist::is_static_procedural_var(&local) {
3922                    continue;
3923                }
3924                if !is_automatic_block_var(&local) {
3925                    b.blocks[entry.0 as usize]
3926                        .bindings
3927                        .push(ArgBinding::Init { var: local });
3928                }
3929                b.inlined_locals.push(local);
3930            }
3931            for v in crate::patterns::pattern_vars(body.as_sem_node()) {
3932                if !b.inlined_locals.iter().any(|l| l.uid() == v.uid()) {
3933                    b.inlined_locals.push(v);
3934                }
3935            }
3936            let after = b.alloc_block();
3937            b.target_depth.insert(after, 0);
3938            b.return_targets.push(after);
3939            // `disable task;` ends every activation (LRM 9.6.2): each one's exit
3940            // is the end of its own kernel.
3941            let task_name = task.name().to_string();
3942            let task_scoped = is_disabled_somewhere(&task_name);
3943            if task_scoped {
3944                b.scope_chain
3945                    .push((task_name.clone(), after, b.frame_stack.len()));
3946                b.disable_targets
3947                    .push((task_name, after, b.frame_stack.len()));
3948            }
3949            // LRM 19.3 / 19.5.5: every activation signals the task's `begin` at
3950            // its first statement and `end` at its exit block (also reached by a
3951            // `disable task;`), like an inlined call.
3952            b.block_event(entry, task, false);
3953            b.event_stack.push((task, b.disable_targets.len()));
3954            let exit = b.walk_stmt(body, entry);
3955            b.event_stack.pop();
3956            if task_scoped {
3957                b.scope_chain.pop();
3958                b.disable_targets.pop();
3959            }
3960            let exit = exit?;
3961            b.close(exit, Terminator::Fallthrough(after));
3962            b.block_event(after, task, true);
3963            for &i in &outs {
3964                b.blocks[after.0 as usize]
3965                    .bindings
3966                    .push(ArgBinding::ToCaptured {
3967                        temp: 2 * n + i as u32,
3968                        formal: formals[i],
3969                    });
3970            }
3971            b.close(after, Terminator::End { loop_back: None });
3972            let inlined_locals = std::mem::take(&mut b.inlined_locals);
3973            let captures = std::mem::take(&mut b.captures);
3974            let capture_types = std::mem::take(&mut b.capture_types);
3975            let b_ref_formals: Vec<Symbol<'d>> = b.ref_stack.iter().map(|&(f, _)| f).collect();
3976            let ref_bindings = b.take_ref_bindings();
3977            let disable_scopes = b.take_disable_scopes();
3978            let disable_broadcast = std::mem::take(&mut b.disable_broadcast);
3979            let fork_branch_names = std::mem::take(&mut b.fork_branch_names);
3980            let mut built = ProcessCfg {
3981                blocks: b.finish(),
3982                entry,
3983                body,
3984                inlined_locals,
3985                captures,
3986                captured: Vec::new(),
3987                ref_bindings,
3988                disable_names: Vec::new(),
3989                fork_branch_names,
3990                disable_scopes,
3991                disable_broadcast,
3992                inherited: Vec::new(),
3993                capture_types,
3994                recursion: Recursion::Root,
3995                share_id: None,
3996            };
3997            built.captured = captured_vars(&built);
3998            built
3999                .captured
4000                .retain(|c| !b_ref_formals.iter().any(|f| f.uid() == c.uid()));
4001            if let Some(k) = key {
4002                built.share_id = Some(share);
4003                self.fork_memo.borrow_mut().insert(k, built.clone());
4004            }
4005            built
4006        };
4007
4008        // The caller: the arguments, evaluated now, into temporaries the
4009        // child inherits by value.
4010        let call = self.alloc_block();
4011        self.close(cur, Terminator::Fallthrough(call));
4012        for (i, (f, &a)) in formals.iter().zip(actuals).enumerate() {
4013            let temp = self.set_capture_type(a, *f);
4014            self.blocks[call.0 as usize]
4015                .bindings
4016                .push(if i == 0 && implicit_this {
4017                    ArgBinding::CaptureThis { temp }
4018                } else {
4019                    ArgBinding::Capture { temp, value: a }
4020                });
4021            branch.inherited.push(InheritedSlot::Temp {
4022                parent: temp,
4023                child: i as u32,
4024            });
4025        }
4026        let join_blk = self.alloc_block();
4027        // The results, back into the actuals once the activation is done.
4028        for &i in &outs {
4029            let ret = self.set_capture_type(actuals[i], formals[i]);
4030            branch.inherited.push(InheritedSlot::Return {
4031                parent: ret,
4032                child: 2 * n + i as u32,
4033            });
4034            self.blocks[join_blk.0 as usize]
4035                .bindings
4036                .push(ArgBinding::AssignCaptured {
4037                    temp: ret,
4038                    target: actuals[i],
4039                    nba: false,
4040                });
4041        }
4042        self.close(
4043            call,
4044            Terminator::ForkJoin {
4045                kind: StatementBlockKind::JoinAll,
4046                branches: vec![branch],
4047                join_blk,
4048            },
4049        );
4050        Ok(join_blk)
4051    }
4052
4053    /// A call of the recursive task inside its own activation
4054    /// ([`Self::inline_recursive_task`]): the arguments go to capture
4055    /// temporaries `n..2n`, and a placeholder branch forks the activation's
4056    /// own kernel again ([`Recursion::SelfSpawn`]), joined at once.
4057    fn recursive_self_call(
4058        &mut self,
4059        actuals: &[Expression<'d>],
4060        cur: BlockId,
4061        recv: Option<Option<Expression<'d>>>,
4062    ) -> Result<BlockId, LoweringError> {
4063        let rec = self
4064            .rec
4065            .as_ref()
4066            .ok_or_else(|| LoweringError::malformed("recursive call outside its activation"))?;
4067        let (n, body, rec_outs) = (rec.n, rec.body, rec.outs.clone());
4068        let skip = usize::from(rec.has_this);
4069        for (a, t) in actuals.iter().zip(&rec.refs[skip..]) {
4070            if let Some(t) = t
4071                && self.ref_storage(call_actual_lvalue(*a)).map(|s| s.uid()) != Some(t.uid())
4072            {
4073                return Err(LoweringError::unsupported(
4074                    "recursive call whose `ref` argument is not the storage the first call \
4075                     bound (a recursive task's `ref` formal is an alias of one static \
4076                     variable or net; LRM 1800-2023 13.5.2)",
4077                ));
4078            }
4079        }
4080        let mut actuals: Vec<Expression<'d>> = actuals
4081            .iter()
4082            .zip(&rec_outs[skip..])
4083            .map(|(&a, &out)| if out { call_actual_lvalue(a) } else { a })
4084            .collect();
4085        // The receiver of a method call is capture 0.
4086        let mut implicit_this = false;
4087        if let Some(r) = recv {
4088            let anchor = self.call_anchor.ok_or_else(|| {
4089                LoweringError::malformed("recursive method call without a call expression")
4090            })?;
4091            implicit_this = r.is_none();
4092            actuals.insert(0, r.unwrap_or(anchor));
4093        }
4094        let actuals = &actuals[..];
4095        if actuals.len() as u32 != n {
4096            return Err(LoweringError::malformed(
4097                "recursive call with the wrong number of arguments",
4098            ));
4099        }
4100        let call = self.alloc_block();
4101        self.close(cur, Terminator::Fallthrough(call));
4102        let mut inherited = Vec::new();
4103        for (i, &a) in actuals.iter().enumerate() {
4104            let temp = n + i as u32;
4105            self.blocks[call.0 as usize]
4106                .bindings
4107                .push(if i == 0 && implicit_this {
4108                    ArgBinding::CaptureThis { temp }
4109                } else {
4110                    ArgBinding::Capture { temp, value: a }
4111                });
4112            inherited.push(InheritedSlot::Temp {
4113                parent: temp,
4114                child: i as u32,
4115            });
4116        }
4117        // The results of the callee, back into the actuals after the join.
4118        let join_blk = self.alloc_block();
4119        for (i, &a) in actuals.iter().enumerate() {
4120            if rec_outs[i] {
4121                let ret = 3 * n + i as u32;
4122                inherited.push(InheritedSlot::Return {
4123                    parent: ret,
4124                    child: 2 * n + i as u32,
4125                });
4126                self.blocks[join_blk.0 as usize]
4127                    .bindings
4128                    .push(ArgBinding::AssignCaptured {
4129                        temp: ret,
4130                        target: a,
4131                        nba: false,
4132                    });
4133            }
4134        }
4135        let mut stub = CfgBuilder::new();
4136        let entry = stub.alloc_block();
4137        stub.close(entry, Terminator::End { loop_back: None });
4138        let branch = ProcessCfg {
4139            blocks: stub.finish(),
4140            entry,
4141            body,
4142            inlined_locals: Vec::new(),
4143            captures: Vec::new(),
4144            captured: Vec::new(),
4145            ref_bindings: Vec::new(),
4146            disable_names: Vec::new(),
4147            fork_branch_names: Vec::new(),
4148            disable_scopes: Vec::new(),
4149            disable_broadcast: Vec::new(),
4150            inherited,
4151            capture_types: Vec::new(),
4152            recursion: Recursion::SelfSpawn,
4153            share_id: None,
4154        };
4155        self.close(
4156            call,
4157            Terminator::ForkJoin {
4158                kind: StatementBlockKind::JoinAll,
4159                branches: vec![branch],
4160                join_blk,
4161            },
4162        );
4163        Ok(join_blk)
4164    }
4165
4166    /// A call, inside one activation, of the task of the enclosing activation
4167    /// `self.rec_up[k]` (mutual recursion, LRM 1800-2023 13.4.1): the
4168    /// arguments go to fresh capture temporaries and a placeholder branch
4169    /// forks that activation's kernel again ([`Recursion::AncestorSpawn`]),
4170    /// joined at once.
4171    fn recursive_ancestor_call(
4172        &mut self,
4173        k: usize,
4174        actuals: &[Expression<'d>],
4175        cur: BlockId,
4176        recv: Option<Option<Expression<'d>>>,
4177    ) -> Result<BlockId, LoweringError> {
4178        let anc = self.rec_up[k].clone();
4179        if anc.refs.iter().any(Option::is_some) {
4180            return Err(LoweringError::unsupported(
4181                "mutually recursive tasks with a `ref` formal (LRM 1800-2023 13.4.1, 13.5.2)",
4182            ));
4183        }
4184        let skip = usize::from(anc.has_this);
4185        let mut actuals: Vec<Expression<'d>> = actuals
4186            .iter()
4187            .zip(&anc.outs[skip..])
4188            .map(|(&a, &out)| if out { call_actual_lvalue(a) } else { a })
4189            .collect();
4190        let mut implicit_this = false;
4191        if let Some(r) = recv {
4192            let anchor = self.call_anchor.ok_or_else(|| {
4193                LoweringError::malformed("recursive method call without a call expression")
4194            })?;
4195            implicit_this = r.is_none();
4196            actuals.insert(0, r.unwrap_or(anchor));
4197        }
4198        if actuals.len() as u32 != anc.n {
4199            return Err(LoweringError::malformed(
4200                "recursive call with the wrong number of arguments",
4201            ));
4202        }
4203        let call = self.alloc_block();
4204        self.close(cur, Terminator::Fallthrough(call));
4205        let mut inherited = Vec::new();
4206        for (i, &a) in actuals.iter().enumerate() {
4207            let temp = self.set_capture_type(a, anc.proto[i].1);
4208            self.blocks[call.0 as usize]
4209                .bindings
4210                .push(if i == 0 && implicit_this {
4211                    ArgBinding::CaptureThis { temp }
4212                } else {
4213                    ArgBinding::Capture { temp, value: a }
4214                });
4215            inherited.push(InheritedSlot::Temp {
4216                parent: temp,
4217                child: i as u32,
4218            });
4219        }
4220        // The ancestor's results, back into the actuals after the join.
4221        let join_blk = self.alloc_block();
4222        for (i, &a) in actuals.iter().enumerate() {
4223            if anc.outs[i] {
4224                let ret = self.set_capture_type(a, anc.proto[i].1);
4225                inherited.push(InheritedSlot::Return {
4226                    parent: ret,
4227                    child: 2 * anc.n + i as u32,
4228                });
4229                self.blocks[join_blk.0 as usize]
4230                    .bindings
4231                    .push(ArgBinding::AssignCaptured {
4232                        temp: ret,
4233                        target: a,
4234                        nba: false,
4235                    });
4236            }
4237        }
4238        let mut stub = CfgBuilder::new();
4239        let entry = stub.alloc_block();
4240        stub.close(entry, Terminator::End { loop_back: None });
4241        let branch = ProcessCfg {
4242            blocks: stub.finish(),
4243            entry,
4244            body: anc.body,
4245            inlined_locals: Vec::new(),
4246            captures: Vec::new(),
4247            captured: Vec::new(),
4248            ref_bindings: Vec::new(),
4249            disable_names: Vec::new(),
4250            fork_branch_names: Vec::new(),
4251            disable_scopes: Vec::new(),
4252            disable_broadcast: Vec::new(),
4253            inherited,
4254            capture_types: Vec::new(),
4255            recursion: Recursion::AncestorSpawn((self.rec_up.len() - k) as u32),
4256            share_id: None,
4257        };
4258        self.close(
4259            call,
4260            Terminator::ForkJoin {
4261                kind: StatementBlockKind::JoinAll,
4262                branches: vec![branch],
4263                join_blk,
4264            },
4265        );
4266        Ok(join_blk)
4267    }
4268
4269    /// A new capture temporary shaped like `formal`, holding `value`.
4270    fn set_capture_type(&mut self, value: Expression<'d>, formal: Symbol<'d>) -> u32 {
4271        let temp = self.captures.len() as u32;
4272        self.captures.push(value);
4273        self.capture_types.resize(temp as usize, None);
4274        self.capture_types.push(Some(formal));
4275        temp
4276    }
4277
4278    /// [`Self::inline_task_call`] of `task` with the arguments `actuals`.
4279    /// An expression statement that calls a value-returning function whose
4280    /// body spawns processes (`fork ... join_none` in a function, LRM
4281    /// 1800-2023 13.4.4), which cannot be compiled: each such call is inlined
4282    /// ahead of the statement (arguments first, in source order) and the
4283    /// statement reads the function's result variable instead of calling it
4284    /// (codegen `lower_user_subroutine_call_of`). `None` when the statement
4285    /// has no such call. The same function twice in one statement, or a call
4286    /// to a virtual method, is refused by name.
4287    fn hoist_suspending_function_calls(
4288        &mut self,
4289        stmt: Statement<'d>,
4290        cur: BlockId,
4291    ) -> Result<Option<BlockId>, LoweringError> {
4292        let Some(cur) = self.hoist_suspending_calls_in(stmt.as_sem_node(), cur)? else {
4293            return Ok(None);
4294        };
4295        self.push(cur, stmt);
4296        Ok(Some(cur))
4297    }
4298
4299    /// [`Self::hoist_suspending_function_calls`]' inlining half over any
4300    /// expression tree: inlines every spawning function call under `node`
4301    /// ahead of the caller, which then reads their result variables. Also
4302    /// used for an inlined function's `return expr;` (`return m(...)`).
4303    fn hoist_suspending_calls_in(
4304        &mut self,
4305        node: SemNode<'d>,
4306        cur: BlockId,
4307    ) -> Result<Option<BlockId>, LoweringError> {
4308        let mut calls = Vec::new();
4309        collect_suspending_function_calls(node, &mut calls);
4310        if calls.is_empty() {
4311            return Ok(None);
4312        }
4313        let mut count: std::collections::HashMap<_, usize> = std::collections::HashMap::new();
4314        for call in &calls {
4315            if let Some(callee) = user_task_callee(call) {
4316                *count.entry(callee.uid()).or_default() += 1;
4317            }
4318        }
4319        let mut cur = cur;
4320        for call in calls {
4321            let Some(callee) = user_task_callee(&call) else {
4322                continue;
4323            };
4324            // A method (LRM 8.6) is inlined with its `this` bound to the
4325            // receiver (`inline_task_call`). A virtual one picks among
4326            // several overrides, each with a result variable of its own.
4327            let is_method =
4328                callee.subroutine_this_var().is_some() || call.call_this_class().is_some();
4329            let flags = callee.subroutine_flags();
4330            let is_virtual = is_method
4331                && (flags.contains(sv_lang::MethodFlags::VIRTUAL)
4332                    || flags.contains(sv_lang::MethodFlags::PURE)
4333                    || callee.subroutine_override().is_some());
4334            // The function's result variable is shared by its calls (and a
4335            // virtual method's overrides have one each): a call that cannot
4336            // read it straight after its inlining parks the value in a
4337            // temporary of its own, which the call's expression reads.
4338            let temp = (is_virtual || count.get(&callee.uid()).copied().unwrap_or(0) > 1)
4339                .then(|| {
4340                    callee
4341                        .subroutine_return_val_var()
4342                        .map(|rv| self.set_capture_type(call, rv))
4343                })
4344                .flatten();
4345            if let Some(temp) = temp {
4346                self.pending_result_temp = Some((temp, call));
4347            }
4348            cur = if is_method {
4349                self.inline_task_call(call, callee, cur)?
4350            } else {
4351                let actuals = call.operands();
4352                let exit = self.inline_task_body(&actuals, callee, cur, None)?;
4353                self.park_call_result(exit, temp.map(|t| (t, call)), callee);
4354                exit
4355            };
4356            self.pending_result_temp = None;
4357            if let Some(temp) = temp {
4358                self.blocks[cur.0 as usize]
4359                    .bindings
4360                    .push(ArgBinding::CallResult { temp, call });
4361            }
4362        }
4363        Ok(Some(cur))
4364    }
4365
4366    fn inline_task_body(
4367        &mut self,
4368        actuals: &[Expression<'d>],
4369        task: Symbol<'d>,
4370        cur: BlockId,
4371        this: Option<(Symbol<'d>, Option<Expression<'d>>)>,
4372    ) -> Result<BlockId, LoweringError> {
4373        if let Some(rec) = &self.rec
4374            && rec.task == task.uid()
4375            && this.is_some() == rec.has_this
4376        {
4377            return self.recursive_self_call(actuals, cur, this.map(|t| t.1));
4378        }
4379        if let Some(k) = self
4380            .rec_up
4381            .iter()
4382            .rposition(|r| r.task == task.uid() && r.has_this == this.is_some())
4383        {
4384            return self.recursive_ancestor_call(k, actuals, cur, this.map(|t| t.1));
4385        }
4386        if self.rec.is_none() && self.rec_outer.contains(&task.uid()) {
4387            // A fork branch the activation does not hand its recursion to
4388            // (a virtual-task or detached branch): inlining would not end.
4389            return Err(LoweringError::unsupported(format!(
4390                "call to task `{}` from a branch of its own recursive activation that cannot \
4391                 spawn it (LRM 1800-2023 13.4.1)",
4392                task.name()
4393            )));
4394        }
4395        if self.inline_stack.contains(&task.uid()) {
4396            return Err(LoweringError::unsupported(format!(
4397                "recursive call to task `{}`, which contains timing controls (it has to be \
4398                 inlined into the calling process)",
4399                task.name()
4400            )));
4401        }
4402        if !self.rec_outer.contains(&task.uid())
4403            && task_reaches_itself(&task, self.virtual_tasks.as_deref())
4404        {
4405            return self.inline_recursive_task(actuals, task, cur, this);
4406        }
4407        let body = proc_body(&task).ok_or_else(|| {
4408            LoweringError::malformed(format!("task `{}` has no body", task.name()))
4409        })?;
4410        let formals: Vec<Symbol<'d>> = task.subroutine_arguments().collect();
4411        if formals.len() != actuals.len() {
4412            return Err(LoweringError::malformed(format!(
4413                "call to task `{}` has {} arguments for {} formals",
4414                task.name(),
4415                actuals.len(),
4416                formals.len()
4417            )));
4418        }
4419        let entry = self.alloc_block();
4420        self.close(cur, Terminator::Fallthrough(entry));
4421        let after = self.alloc_block();
4422        self.target_depth.insert(after, self.owned_scopes.len());
4423        let frame_mark = self.inlined_locals.len();
4424        let mut releases = Vec::new();
4425        if let Some((this_var, receiver)) = this {
4426            let binding = match receiver {
4427                Some(r) => ArgBinding::In {
4428                    formal: this_var,
4429                    actual: r,
4430                },
4431                None => ArgBinding::ThisOf { var: this_var },
4432            };
4433            self.blocks[entry.0 as usize].bindings.push(binding);
4434            self.inlined_locals.push(this_var);
4435            // Both bindings hold one count on the object for the call.
4436            releases.push(ArgBinding::Release { var: this_var });
4437        }
4438        let mut copy_backs = Vec::new();
4439        let mut aliases = Vec::new();
4440        let mut bound_refs: Vec<(Symbol<'d>, Expression<'d>)> = Vec::new();
4441        for (formal, &actual) in formals.iter().zip(actuals) {
4442            // slang binds an output/inout actual as `actual = <formal>`.
4443            let lvalue = if actual.kind() == ExpressionKind::Assignment {
4444                actual.left().ok_or_else(|| {
4445                    LoweringError::malformed("output argument with no target expression")
4446                })?
4447            } else {
4448                actual
4449            };
4450            // A `ref` naming static storage (directly or through an
4451            // enclosing call's aliased `ref`) is that storage in the body.
4452            // (An `event` (or event array) ref formal is a handle: copied in and back out, like
4453            // `inout`, so the body's `r = e` merges the actual's event.)
4454            if formal.formal_argument_direction() == sv_lang::ArgumentDirection::Ref
4455                && !formal
4456                    .value_type()
4457                    .as_ref()
4458                    .is_some_and(|t| crate::state::event_pool::event_count(t).is_some())
4459                && let Some(target) = self.ref_storage(lvalue)
4460            {
4461                self.ref_candidates.push((*formal, Some(target)));
4462                aliases.push((*formal, target));
4463                continue;
4464            }
4465            // A `ref` to a queue / dynamic-array element of a packed or real
4466            // type (LRM 1800-2023 13.5.2): the formal is a live alias, every
4467            // access of it goes through a runtime reference
4468            // ([`ArgBinding::RefOpen`]); other element types keep the
4469            // copy-in/copy-out of `inout`.
4470            if formal.formal_argument_direction() == sv_lang::ArgumentDirection::Ref
4471                && lvalue.kind() == ExpressionKind::ElementSelect
4472                && formal
4473                    .value_type()
4474                    .is_some_and(|t| t.is_integral() || t.is_floating())
4475                && lvalue
4476                    .select_value()
4477                    .and_then(|b| b.expr_type())
4478                    .is_some_and(|t| t.is_dynamically_sized_array() || t.is_associative_array())
4479            {
4480                // One reference-id slot per formal, shared by every call
4481                // site: the body reaches the reference through the formal.
4482                let prior =
4483                    self.blocks
4484                        .iter()
4485                        .flat_map(|b| b.bindings.iter())
4486                        .find_map(|b| match b {
4487                            ArgBinding::RefOpen {
4488                                temp, formal: f, ..
4489                            } if f.uid() == formal.uid() => Some(*temp),
4490                            _ => None,
4491                        });
4492                let temp = prior.unwrap_or_else(|| {
4493                    self.captures.push(lvalue);
4494                    self.captures.len() as u32 - 1
4495                });
4496                self.blocks[entry.0 as usize]
4497                    .bindings
4498                    .push(ArgBinding::RefOpen {
4499                        temp,
4500                        formal: *formal,
4501                        actual: lvalue,
4502                    });
4503                copy_backs.push(ArgBinding::RefClose {
4504                    temp,
4505                    formal: *formal,
4506                    actual: lvalue,
4507                });
4508                self.ref_candidates.push((*formal, None));
4509                if !crate::netlist::is_static_procedural_var(formal) {
4510                    self.inlined_locals.push(*formal);
4511                }
4512                continue;
4513            }
4514            let binding = match formal.formal_argument_direction() {
4515                sv_lang::ArgumentDirection::In => ArgBinding::In {
4516                    formal: *formal,
4517                    actual,
4518                },
4519                sv_lang::ArgumentDirection::Out => ArgBinding::Init { var: *formal },
4520                sv_lang::ArgumentDirection::InOut | sv_lang::ArgumentDirection::Ref => {
4521                    ArgBinding::In {
4522                        formal: *formal,
4523                        actual: lvalue,
4524                    }
4525                }
4526            };
4527            self.blocks[entry.0 as usize].bindings.push(binding);
4528            if formal.formal_argument_direction() == sv_lang::ArgumentDirection::Ref {
4529                bound_refs.push((*formal, lvalue));
4530                let named = (crate::symid::is_value_name(lvalue.kind())
4531                    && !formal
4532                        .value_type()
4533                        .as_ref()
4534                        .is_some_and(|t| crate::state::event_pool::event_count(t).is_some()))
4535                .then(|| lvalue.referenced_symbol())
4536                .flatten();
4537                self.ref_candidates.push((*formal, named));
4538            }
4539            if formal.formal_argument_direction() != sv_lang::ArgumentDirection::In {
4540                copy_backs.push(ArgBinding::Out {
4541                    actual: lvalue,
4542                    formal: *formal,
4543                });
4544            }
4545            // A static task's formal is the instance's (a signal).
4546            if !crate::netlist::is_static_procedural_var(formal) {
4547                self.inlined_locals.push(*formal);
4548                if formal.formal_argument_direction() == sv_lang::ArgumentDirection::In {
4549                    releases.push(ArgBinding::Release { var: *formal });
4550                }
4551            }
4552        }
4553        // A method's body (and its locals' initializers) runs against its
4554        // own `this`, in blocks of its own; the bindings above ran in the
4555        // caller's.
4556        if let Some((this_var, _)) = this {
4557            self.this_stack.push(this_var);
4558        }
4559        let n_aliases = aliases.len();
4560        self.ref_stack.extend(aliases);
4561        let body_entry = if this.is_some() || n_aliases > 0 {
4562            let b = self.alloc_block();
4563            self.close(entry, Terminator::Fallthrough(b));
4564            b
4565        } else {
4566            entry
4567        };
4568        for local in task_locals(&task) {
4569            // A method's `this` is bound above, never a default-initialized
4570            // local.
4571            if this.is_some_and(|(t, _)| t.uid() == local.uid()) {
4572                continue;
4573            }
4574            // A static variable is the instance's (a signal); a block's
4575            // automatic variables are initialized on entry to it.
4576            if crate::netlist::is_static_procedural_var(&local) {
4577                continue;
4578            }
4579            if !is_automatic_block_var(&local) {
4580                self.blocks[body_entry.0 as usize]
4581                    .bindings
4582                    .push(ArgBinding::Init { var: local });
4583                releases.push(ArgBinding::Release { var: local });
4584            }
4585            self.inlined_locals.push(local);
4586        }
4587        // An inlined value-returning function (a `fork` in its body cannot be
4588        // compiled, LRM 13.4.4): its result variable is a local of the
4589        // process, which `return expr;` / `f = expr;` assign and the hoisted
4590        // call's expression reads (`hoist_suspending_function_calls`).
4591        let ret_var = if task.subroutine_kind() == Some(sv_lang::SubroutineKind::Function)
4592            && task.subroutine_return_type().is_some_and(|t| !t.is_void())
4593        {
4594            task.subroutine_return_val_var()
4595        } else {
4596            None
4597        };
4598        if let Some(rv) = ret_var {
4599            self.blocks[body_entry.0 as usize]
4600                .bindings
4601                .push(ArgBinding::Init { var: rv });
4602            self.inlined_locals.push(rv);
4603        }
4604        // The pattern variables of the body's `matches` (LRM 12.6).
4605        for v in crate::patterns::pattern_vars(body.as_sem_node()) {
4606            if !self.inlined_locals.iter().any(|l| l.uid() == v.uid()) {
4607                self.inlined_locals.push(v);
4608            }
4609        }
4610        // VPI frame (37.43): the formals and locals this inlining made
4611        // automatic variables of the process, in that order.
4612        let frame_vars: Vec<Symbol<'d>> = self.inlined_locals[frame_mark..]
4613            .iter()
4614            .copied()
4615            .filter(|v| this.is_none_or(|(t, _)| t.uid() != v.uid()))
4616            .filter(|v| !bound_refs.iter().any(|(f, _)| f.uid() == v.uid()))
4617            .collect();
4618        self.frame_probe(
4619            body_entry,
4620            FrameProbe::Enter {
4621                task,
4622                vars: frame_vars.clone(),
4623                bound: bound_refs,
4624            },
4625        );
4626        self.block_event(body_entry, task, false);
4627        self.event_stack.push((task, self.disable_targets.len()));
4628        self.frame_stack.push((task, frame_vars.clone()));
4629        self.inline_stack.push(task.uid());
4630        self.return_targets.push(after);
4631        self.ret_var_stack.push(ret_var);
4632        // `disable task;` ends this activation (LRM 9.6.2), from inside it or
4633        // from another process: the exit is the block after the call, whose
4634        // frame probe ends the activation's VPI frame.
4635        let task_name = task.name().to_string();
4636        let task_scoped = is_disabled_somewhere(&task_name);
4637        let mut n_tags = 0;
4638        if task_scoped {
4639            self.scope_chain
4640                .push((task_name.clone(), after, self.frame_stack.len()));
4641            self.disable_targets
4642                .push((task_name, after, self.frame_stack.len()));
4643            // LRM 25.7.4: an `extern` method's activation is also the scope
4644            // `disable m1.a.t;` (this module instance's call) and `disable
4645            // b.t;` (every call through the interface instance) name.
4646            for tag in crate::netlist::itf::extern_scope_tags(&task) {
4647                self.scope_chain.push((tag, after, self.frame_stack.len()));
4648                n_tags += 1;
4649            }
4650        }
4651        let exit = self.walk_stmt(body, body_entry);
4652        if task_scoped {
4653            self.scope_chain.pop();
4654            self.disable_targets.pop();
4655            for _ in 0..n_tags {
4656                self.scope_chain.pop();
4657            }
4658        }
4659        self.event_stack.pop();
4660        self.return_targets.pop();
4661        self.ret_var_stack.pop();
4662        self.inline_stack.pop();
4663        self.frame_stack.pop();
4664        if this.is_some() {
4665            self.this_stack.pop();
4666        }
4667        self.ref_stack.truncate(self.ref_stack.len() - n_aliases);
4668        let exit = exit?;
4669        self.close(exit, Terminator::Fallthrough(after));
4670        self.blocks[after.0 as usize].bindings.extend(copy_backs);
4671        self.blocks[after.0 as usize].bindings.extend(releases);
4672        self.block_event(after, task, true);
4673        self.frame_probe(
4674            after,
4675            FrameProbe::Exit {
4676                task,
4677                vars: frame_vars,
4678            },
4679        );
4680        Ok(after)
4681    }
4682
4683    /// An immediate assertion whose pass or fail statement waits (LRM 16.3):
4684    /// [`Terminator::Assertion`] over the two statements' own blocks.
4685    fn walk_immediate_assertion(
4686        &mut self,
4687        stmt: Statement<'d>,
4688        (pass_stmt, fail_stmt): (Option<Statement<'d>>, Option<Statement<'d>>),
4689        cur: BlockId,
4690    ) -> Result<BlockId, LoweringError> {
4691        if stmt.is_deferred_assertion() == Some(true) {
4692            return Err(LoweringError::unsupported(
4693                "a deferred assertion (LRM 16.4) whose action block waits (the action must be \
4694                 a subroutine call that is queued)"
4695                    .to_string(),
4696            ));
4697        }
4698        let pass_blk = self.alloc_block();
4699        let fail_blk = self.alloc_block();
4700        let done_blk = self.alloc_block();
4701        self.close(
4702            cur,
4703            Terminator::Assertion {
4704                stmt,
4705                pass_blk,
4706                fail_blk,
4707                done_blk,
4708            },
4709        );
4710        for (action, blk) in [(pass_stmt, pass_blk), (fail_stmt, fail_blk)] {
4711            let exit = match action {
4712                Some(a) => self.walk_stmt(a, blk)?,
4713                None => blk,
4714            };
4715            self.close(exit, Terminator::Fallthrough(done_blk));
4716        }
4717        Ok(done_blk)
4718    }
4719
4720    /// A `case` whose arms need their own blocks ([`Terminator::Case`]).
4721    fn walk_case(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
4722        // A `case ... matches` has its own item and default accessors.
4723        let is_pattern = stmt.kind() == StatementKind::PatternCase;
4724        // A constant selector over arms one of which slang demoted to
4725        // `Invalid` (a statically dead arm): only the matching arm can run
4726        // (LRM 12.5), so lower just it.
4727        if !is_pattern && let Some(live) = constant_case_live_arm(&stmt) {
4728            return match live {
4729                Some(arm) => self.walk_stmt(arm, cur),
4730                None => Ok(cur),
4731            };
4732        }
4733        let arm_stmts: Vec<Statement<'d>> = if is_pattern {
4734            stmt.pattern_case_items().iter().map(|i| i.stmt()).collect()
4735        } else {
4736            stmt.case_items().iter().map(|i| i.stmt()).collect()
4737        };
4738        let arms: Vec<BlockId> = arm_stmts.iter().map(|_| self.alloc_block()).collect();
4739        let default_blk = self.alloc_block();
4740        let join = self.alloc_block();
4741        self.close(
4742            cur,
4743            Terminator::Case {
4744                stmt,
4745                arms: arms.clone(),
4746                default_blk,
4747            },
4748        );
4749        for (arm_stmt, &arm) in arm_stmts.iter().zip(&arms) {
4750            let exit = self.walk_stmt(*arm_stmt, arm)?;
4751            self.close(exit, Terminator::Fallthrough(join));
4752        }
4753        let default_stmt = if is_pattern {
4754            stmt.pattern_case_default()
4755        } else {
4756            stmt.default_case()
4757        };
4758        let default_exit = match default_stmt {
4759            Some(d) => self.walk_stmt(d, default_blk)?,
4760            None => default_blk,
4761        };
4762        self.close(default_exit, Terminator::Fallthrough(join));
4763        Ok(join)
4764    }
4765
4766    /// Walks a loop body with `continue` going to `cont` and `break` to
4767    /// `brk`.
4768    fn walk_loop_body(
4769        &mut self,
4770        body_stmt: Statement<'d>,
4771        body: BlockId,
4772        cont: BlockId,
4773        brk: BlockId,
4774    ) -> Result<BlockId, LoweringError> {
4775        self.loop_targets.push((cont, brk));
4776        let open = self.owned_scopes.len();
4777        self.target_depth.insert(cont, open);
4778        self.target_depth.insert(brk, open);
4779        let exit = self.walk_stmt(body_stmt, body);
4780        self.loop_targets.pop();
4781        exit
4782    }
4783
4784    /// Walks a block, keeping [`Self::name_path`] current for the sequential
4785    /// block's own name.
4786    fn walk_block(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
4787        // `L: assert (..) ... disable L; ...`: one leaf (see the helper).
4788        if !self.entering_block && self_disabling_assertion_block(&stmt) {
4789            self.push(cur, stmt);
4790            return Ok(cur);
4791        }
4792        let name = (!self.entering_block
4793            && stmt.block_kind() == Some(StatementBlockKind::Sequential))
4794        .then(|| block_name(&stmt))
4795        .filter(|n| !n.is_empty());
4796        let pushed = name.is_some();
4797        if let Some(n) = name {
4798            self.name_path.push(n);
4799        }
4800        // LRM 19.3: `@@(begin blk)` / `@@(end blk)` of a named block, signalled
4801        // at its first statement and after its last (the codegen drops the
4802        // ones no covergroup names). Only a block that is walked into the CFG
4803        // gets here; a leaf block's events are emitted where it is lowered.
4804        let blk = (!self.entering_block
4805            && stmt.block_kind() == Some(StatementBlockKind::Sequential))
4806        .then(|| stmt.block_symbol())
4807        .flatten()
4808        .filter(|b| !b.name().is_empty());
4809        if let Some(b) = blk {
4810            self.block_event(cur, b, false);
4811            self.event_stack.push((b, self.disable_targets.len()));
4812        }
4813        let r = self.walk_block_inner(stmt, cur);
4814        if blk.is_some() {
4815            self.event_stack.pop();
4816        }
4817        if pushed {
4818            self.name_path.pop();
4819        }
4820        if let (Some(b), Ok(end)) = (blk, &r) {
4821            self.block_event(*end, b, true);
4822        }
4823        r
4824    }
4825
4826    fn walk_block_inner(
4827        &mut self,
4828        stmt: Statement<'d>,
4829        cur: BlockId,
4830    ) -> Result<BlockId, LoweringError> {
4831        // `StatementBlockKind` is a plain (non-`#[non_exhaustive]`) enum
4832        // hand-authored in `sv-lang`, so this literal `match` is genuinely,
4833        // compiler-checked exhaustive -- no wildcard needed or used.
4834        match stmt.block_kind() {
4835            // A named block some `disable` inside it can leave: its own exit.
4836            // A block declaring automatic variables: they are created and
4837            // initialized on each entry (LRM 1800-2023 §6.21), at the
4838            // start of a CFG block of its own.
4839            Some(_) if !self.entering_block && !block_automatic_vars(&stmt, true).is_empty() => {
4840                // A CFG block with no statements yet takes the bindings
4841                // itself (they run before its statements).
4842                let fresh = &self.blocks[cur.0 as usize];
4843                let entry = if fresh.stmts.is_empty() && fresh.step_exprs.is_empty() {
4844                    cur
4845                } else {
4846                    let entry = self.alloc_block();
4847                    self.close(cur, Terminator::Fallthrough(entry));
4848                    entry
4849                };
4850                for var in block_automatic_vars(&stmt, true) {
4851                    self.blocks[entry.0 as usize]
4852                        .bindings
4853                        .push(ArgBinding::Init { var });
4854                }
4855                // A sequential block's handle-holding variables end with it.
4856                let owned: Vec<Symbol<'d>> =
4857                    if stmt.block_kind() == Some(StatementBlockKind::Sequential) {
4858                        block_automatic_vars(&stmt, true)
4859                            .into_iter()
4860                            .filter(|v| v.value_type().is_some_and(|t| may_hold_handles(&t)))
4861                            .collect()
4862                    } else {
4863                        Vec::new()
4864                    };
4865                let scoped = !owned.is_empty();
4866                if scoped {
4867                    self.owned_scopes.push(owned.clone());
4868                }
4869                self.entering_block = true;
4870                let r = self.walk_block(stmt, entry);
4871                self.entering_block = false;
4872                if !scoped {
4873                    return r;
4874                }
4875                self.owned_scopes.pop();
4876                let end = r?;
4877                let leave = self.alloc_block();
4878                self.close(end, Terminator::Fallthrough(leave));
4879                for var in owned.into_iter().rev() {
4880                    self.blocks[leave.0 as usize]
4881                        .bindings
4882                        .push(ArgBinding::Release { var });
4883                }
4884                Ok(leave)
4885            }
4886            Some(StatementBlockKind::Sequential)
4887                if let name = block_name(&stmt)
4888                    && !name.is_empty()
4889                    && (is_disabled_somewhere(&name)
4890                        || is_disabled_somewhere(&self.name_path.join("."))
4891                        || contains_disable_through_calls(stmt.as_sem_node())) =>
4892            {
4893                self.entering_block = false;
4894                let exit = self.alloc_block();
4895                self.target_depth.insert(exit, self.owned_scopes.len());
4896                // LRM 9.6.2: another process's `disable` of this block resumes
4897                // the process at `exit`, wherever in the block it is suspended.
4898                let scoped = is_disabled_somewhere(&name);
4899                if scoped {
4900                    self.scope_chain
4901                        .push((name.clone(), exit, self.frame_stack.len()));
4902                }
4903                // `disable p.b.x;` of another process names the block by its
4904                // path from the process's outermost block: a second scope
4905                // under that dotted label tells same-named blocks apart.
4906                let dotted = self.name_path.join(".");
4907                let path_scoped = dotted.contains('.') && is_disabled_somewhere(&dotted);
4908                if path_scoped {
4909                    self.scope_chain
4910                        .push((dotted.clone(), exit, self.frame_stack.len()));
4911                }
4912                // `disable fl.b1.blk;` goes through the fork's own label `fl`
4913                // (9.6.2): the branch's path under that label tells the
4914                // same-named blocks of sibling branches apart.
4915                let fork_key = self
4916                    .fork_name
4917                    .as_ref()
4918                    .map(|f| format!("{f}.{dotted}"))
4919                    .filter(|k| is_disabled_somewhere(k));
4920                let fork_scoped = fork_key.is_some();
4921                if let Some(key) = fork_key {
4922                    self.scope_chain.push((key, exit, self.frame_stack.len()));
4923                }
4924                self.disable_targets
4925                    .push((name, exit, self.frame_stack.len()));
4926                let end = self.walk_seq(live_statements(&stmt), cur);
4927                self.disable_targets.pop();
4928                if fork_scoped {
4929                    self.scope_chain.pop();
4930                }
4931                if path_scoped {
4932                    self.scope_chain.pop();
4933                }
4934                if scoped {
4935                    self.scope_chain.pop();
4936                }
4937                self.close(end?, Terminator::Fallthrough(exit));
4938                Ok(exit)
4939            }
4940            Some(StatementBlockKind::Sequential) => {
4941                self.entering_block = false;
4942                self.walk_seq(live_statements(&stmt), cur)
4943            }
4944            Some(
4945                kind @ (StatementBlockKind::JoinAll
4946                | StatementBlockKind::JoinAny
4947                | StatementBlockKind::JoinNone),
4948            ) => {
4949                // Task 1 (PR-069/`docs/design/process.md` §7): construction
4950                // only, never scheduled. Each branch is walked as its own,
4951                // fully independent `ProcessCfg` (`OneShot` -- a fork
4952                // branch runs to completion or suspends, but never
4953                // re-triggers the way an `always` process does).
4954                //
4955                // Verified live: `>1` un-`begin`/`end`-wrapped branches
4956                // (`fork a; b; join`) are NOT `stmt.statements()`'s direct
4957                // elements -- slang groups them into one `List` node first
4958                // (the same generic multi-statement wrapper `walk_stmt`'s
4959                // own `StatementKind::List` arm already flattens elsewhere),
4960                // so `statements()` here returns exactly one `List` element,
4961                // not one element per branch. A single, un-wrapped branch
4962                // (`fork a; join`) has no such wrapper. Flatten exactly one
4963                // `List` level when present, never more (a `List`
4964                // containing a nested `List` is not a shape any fixture
4965                // this task constructs, so silently over-flattening would
4966                // risk merging two real branches into one).
4967                let branch_stmts = live_statements(&stmt);
4968                let branch_stmts = match branch_stmts.as_slice() {
4969                    [only] if only.kind() == StatementKind::List => live_statements(only),
4970                    _ => branch_stmts,
4971                };
4972                self.entering_block = false;
4973                let fork_name = Some(block_name(&stmt)).filter(|n| !n.is_empty());
4974                // LRM 9.6.2: a `disable` of one branch's own named outermost
4975                // block, reachable from the parent (after this fork
4976                // statement), a sibling branch, or deeper still (threaded
4977                // into each branch's own `Inherited::known_fork_labels`
4978                // below) -- see `CfgBuilder::fork_branch_names`'s own doc
4979                // comment. Computed from a reference: `branch_stmts` is
4980                // still needed, un-consumed, by the `.into_iter()` build
4981                // below.
4982                // The fork block's own name (`fork : NAME`) tags every branch
4983                // process too (`ProcessCfg::disable_names`), so a later
4984                // `disable NAME;` of the parent, after the join, ends the
4985                // branches still running (LRM 9.6.2: all activities enabled
4986                // within the named block end).
4987                let new_branch_names: Vec<String> = branch_stmts
4988                    .iter()
4989                    .filter_map(|s| peer_block(s).map(|p| p.name))
4990                    .collect();
4991                let known_fork_labels: Vec<String> = self
4992                    .fork_branch_names
4993                    .iter()
4994                    .filter(|n| !self.fork_block_names.contains(n))
4995                    .cloned()
4996                    .chain(new_branch_names.iter().cloned())
4997                    .collect();
4998                let ancestor_blocks: Vec<String> = self
4999                    .ancestor_blocks
5000                    .iter()
5001                    .cloned()
5002                    .chain(self.disable_targets.iter().map(|(n, _, _)| n.clone()))
5003                    .collect();
5004                // Equal inherited context and statement give an equal CFG
5005                // (a fresh builder is seeded from `Inherited` alone), so a
5006                // task inlined at many call sites builds each of its forks'
5007                // branches once and the lowering compiles each once
5008                // ([`ProcessCfg::share_id`]). Not inside a recursive
5009                // activation (its capture temporaries are per site) or
5010                // under a clock inference.
5011                let memoizable = self.rec.is_none()
5012                    && self.rec_up.is_empty()
5013                    && self.global_clock.is_none()
5014                    && self.checker_clock.is_none();
5015                let mut branches: Vec<ProcessCfg<'d>> = branch_stmts
5016                    .into_iter()
5017                    .map(|branch_stmt| {
5018                        let key = memoizable.then(|| {
5019                            MemoKey::Fork(ForkBranchKey {
5020                                stmt: (stmt_addr(&branch_stmt), branch_stmt.kind() as u16),
5021                                fork_name: fork_name.clone(),
5022                                default_clocking: self.default_clocking.map(|c| c.uid()),
5023                                this_var: self.this_stack.last().map(|t| t.uid()),
5024                                ref_aliases: self
5025                                    .ref_stack
5026                                    .iter()
5027                                    .map(|(a, b)| (a.uid(), b.uid()))
5028                                    .collect(),
5029                                rec_outer: self.rec_outer.clone(),
5030                                labels: known_fork_labels.clone(),
5031                                ancestors: ancestor_blocks.clone(),
5032                            })
5033                        });
5034                        if let Some(k) = &key
5035                            && let Some(hit) = self.fork_memo.borrow().get(k)
5036                        {
5037                            return Ok(hit.clone());
5038                        }
5039                        let mut cfg = build_cfg_in(
5040                            &branch_stmt,
5041                            ProcessLoopKind::OneShot,
5042                            fork_name.clone(),
5043                            self.default_clocking,
5044                            None,
5045                            self.checker_clock,
5046                            Inherited {
5047                                this_var: self.this_stack.last().copied(),
5048                                ref_aliases: self.ref_stack.clone(),
5049                                global_clock: self.global_clock,
5050                                detached_trigger: false,
5051                                forced_extern: None,
5052                                rec_outer: self.rec_outer.clone(),
5053                                fork_memo: Some(self.fork_memo.clone()),
5054                                rec: self.rec.clone(),
5055                                rec_up: self.rec_up.clone(),
5056                                known_fork_labels: known_fork_labels.clone(),
5057                                ancestor_blocks: ancestor_blocks.clone(),
5058                            },
5059                            self.virtual_tasks.clone(),
5060                        )?;
5061                        if let Some(k) = key {
5062                            cfg.share_id = Some(
5063                                NEXT_SHARE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
5064                            );
5065                            self.fork_memo.borrow_mut().insert(k, cfg.clone());
5066                        }
5067                        Ok(cfg)
5068                    })
5069                    .collect::<Result<_, LoweringError>>()?;
5070                for b in &mut branches {
5071                    b.captured = captured_vars(b);
5072                    // An aliased `ref` formal is the branch's storage too.
5073                    b.captured
5074                        .retain(|c| !self.ref_stack.iter().any(|(f, _)| f.uid() == c.uid()));
5075                    // A branch inside an inlined class-method task shares the
5076                    // forking activation's `this`.
5077                    if let Some(t) = self.this_stack.last()
5078                        && !b.captured.iter().any(|c| c.uid() == t.uid())
5079                    {
5080                        b.captured.push(*t);
5081                    }
5082                }
5083                // The parent keeps these labels resolvable for the rest of
5084                // its own walk (not popped like `disable_targets`): LRM
5085                // 9.6.2 lets a branch be disabled for the rest of the
5086                // process's life, not only while this `fork` statement is
5087                // still being walked.
5088                self.fork_branch_names.extend(new_branch_names);
5089                if let Some(f) = &fork_name {
5090                    self.fork_branch_names.push(f.clone());
5091                    self.fork_block_names.push(f.clone());
5092                }
5093                let join_blk = self.alloc_block();
5094                self.close(
5095                    cur,
5096                    Terminator::ForkJoin {
5097                        kind,
5098                        branches,
5099                        join_blk,
5100                    },
5101                );
5102                Ok(join_blk)
5103            }
5104            None => Err(LoweringError::malformed(
5105                "Block statement with no block_kind (unexpected AST shape)",
5106            )),
5107        }
5108    }
5109
5110    fn walk_seq(
5111        &mut self,
5112        stmts: Vec<Statement<'d>>,
5113        mut cur: BlockId,
5114    ) -> Result<BlockId, LoweringError> {
5115        // LRM 14.11: a `##0` whose clocking event already occurred in this
5116        // time step "shall have no effect". Statically known exactly when the
5117        // process was just released by that event (`##N;` or
5118        // `@(cb);` on the default clocking, a `##0` included once it has waited) and only untimed, call-free
5119        // statements ran since; any other `##0` waits for the event (which is
5120        // right whenever the event has not yet fired this step).
5121        let mut released = false;
5122        for s in stmts {
5123            let s = unwrap_invalid(s);
5124            match self.default_clocking_wait(&s) {
5125                Some(0) if released => {
5126                    if let Some(body) = s.body()
5127                        && body.kind() != StatementKind::Empty
5128                    {
5129                        cur = self.walk_stmt(body, cur)?;
5130                    }
5131                    continue;
5132                }
5133                Some(_) => released = s.body().is_none_or(|b| b.kind() == StatementKind::Empty),
5134                None => {
5135                    released = released && Self::untimed_simple(&s);
5136                }
5137            }
5138            cur = self.walk_stmt(s, cur)?;
5139        }
5140        Ok(cur)
5141    }
5142
5143    /// The cycle count of `##N` / `@(cb)` (count 1) on the default clocking
5144    /// when `stmt` is such a timed statement (see [`Self::walk_seq`]).
5145    fn default_clocking_wait(&self, stmt: &Statement<'d>) -> Option<u32> {
5146        let cb = self.default_clocking?;
5147        if stmt.kind() != StatementKind::Timed {
5148            return None;
5149        }
5150        let tc = stmt.timing_control()?;
5151        // A predicate, not a lowering: any other timing control is simply not
5152        // a wait on the default clocking (the next `##0` then waits).
5153        if tc.kind() == TimingControlKind::CycleDelay {
5154            first_expression_child(&tc)?
5155                .constant_value()?
5156                .as_i64()
5157                .and_then(|v| u32::try_from(v).ok())
5158        } else if tc.kind() == TimingControlKind::SignalEvent && tc.edge() == EdgeKind::None {
5159            let sym = first_expression_child(&tc)?.referenced_symbol()?;
5160            (sym.kind() == SymbolKind::ClockingBlock && sym.uid() == cb.uid()).then_some(1)
5161        } else {
5162            None
5163        }
5164    }
5165
5166    /// A statement that takes no time and calls no user subroutine: a system
5167    /// call or an untimed assignment.
5168    fn untimed_simple(stmt: &Statement<'d>) -> bool {
5169        stmt.kind() == StatementKind::ExpressionStatement
5170            && stmt.expr().is_some_and(|e| {
5171                (e.kind() == ExpressionKind::Call && e.is_system_call() == Some(true))
5172                    || (e.kind() == ExpressionKind::Assignment && e.assignment_timing().is_none())
5173            })
5174    }
5175
5176    /// PR-044: `if`/`else` always expands to `Terminator::Branch`, regardless
5177    /// of whether either branch contains a suspend -- this is orthogonal to
5178    /// (never triggers) suspend splitting, which is exactly what
5179    /// `if_else_does_not_split_the_cfg` (`tests/process_cfg.rs`) proves: the
5180    /// number of *additional* blocks a wrapping suspend contributes is the
5181    /// same with or without the branch present.
5182    fn walk_conditional(
5183        &mut self,
5184        stmt: Statement<'d>,
5185        cur: BlockId,
5186    ) -> Result<BlockId, LoweringError> {
5187        if violation_check(&stmt).is_some() || is_clause_if(&stmt) {
5188            return self.walk_checked_if(stmt, cur);
5189        }
5190        let cond = stmt.expressions().into_iter().next().ok_or_else(|| {
5191            LoweringError::malformed("Conditional statement with no condition expression")
5192        })?;
5193        let then_stmt = stmt
5194            .then_branch()
5195            .ok_or_else(|| LoweringError::malformed("Conditional statement with no then-branch"))?;
5196        let else_stmt = stmt.else_branch();
5197
5198        // A constant predicate picks its arm at elaboration; only the live
5199        // arm becomes blocks (see `constant_if_arm`).
5200        if let Some(taken) = constant_if_arm(&stmt) {
5201            return match if taken { Some(then_stmt) } else { else_stmt } {
5202                Some(live) => self.walk_stmt(live, cur),
5203                None => Ok(cur),
5204            };
5205        }
5206
5207        // A call in the condition to a function that is inlined (a `fork` in
5208        // its body, an event output formal) runs ahead of the test; the
5209        // condition reads its result variable.
5210        let cur = self
5211            .hoist_suspending_calls_in(cond.as_sem_node(), cur)?
5212            .unwrap_or(cur);
5213        let then_blk = self.alloc_block();
5214        let else_blk = self.alloc_block();
5215        self.close(
5216            cur,
5217            Terminator::Branch {
5218                cond,
5219                then_blk,
5220                else_blk,
5221            },
5222        );
5223
5224        let then_exit = self.walk_stmt(then_stmt, then_blk)?;
5225        let else_exit = match else_stmt {
5226            Some(e) => self.walk_stmt(e, else_blk)?,
5227            None => else_blk,
5228        };
5229
5230        let join = self.alloc_block();
5231        self.close(then_exit, Terminator::Fallthrough(join));
5232        self.close(else_exit, Terminator::Fallthrough(join));
5233        Ok(join)
5234    }
5235
5236    /// A `unique`/`unique0`/`priority` `if` series (§12.4.2): one
5237    /// [`Terminator::Case`] over the whole series, `arms[i]` the then-statement
5238    /// of its i-th condition, `default_blk` the final `else` (empty without
5239    /// one), so the dispatch can report a violation (§12.4.2.1) where the
5240    /// series executes.
5241    fn walk_checked_if(
5242        &mut self,
5243        stmt: Statement<'d>,
5244        cur: BlockId,
5245    ) -> Result<BlockId, LoweringError> {
5246        let chain = if_series(stmt).map_err(|why| {
5247            LoweringError::unsupported(format!("{why} in an if series (malformed statement)"))
5248        })?;
5249        let arms: Vec<BlockId> = chain.thens.iter().map(|_| self.alloc_block()).collect();
5250        let default_blk = self.alloc_block();
5251        let join = self.alloc_block();
5252        self.close(
5253            cur,
5254            Terminator::Case {
5255                stmt,
5256                arms: arms.clone(),
5257                default_blk,
5258            },
5259        );
5260        for (then_stmt, &arm) in chain.thens.iter().zip(&arms) {
5261            let exit = self.walk_stmt(*then_stmt, arm)?;
5262            self.close(exit, Terminator::Fallthrough(join));
5263        }
5264        let default_exit = match chain.otherwise {
5265            Some(e) => self.walk_stmt(e, default_blk)?,
5266            None => default_blk,
5267        };
5268        self.close(default_exit, Terminator::Fallthrough(join));
5269        Ok(join)
5270    }
5271
5272    /// PR-006/PR-007: the one place a `BasicBlock` closes with
5273    /// `Terminator::Suspend` from a `Timed` statement. `Delay`/`SignalEvent`
5274    /// (posedge/negedge only) are the slice-scope half; Task 1 adds
5275    /// `EventList` (as `SuspendPoint::EdgeList`) and `CycleDelay`. **Task 6
5276    /// (CG-081/CG-171)** adds real handling for `ImplicitEvent` (as
5277    /// `SuspendPoint::ImplicitComb`) and `OneStepDelay` (as `SuspendPoint::
5278    /// OneStep`), and revises `CycleDelay` to carry its inferred clock
5279    /// signal/edge as real payload instead of discarding it. `Delay3` (not
5280    /// applicable to a procedural suspend point at all), `BlockEventList` (a
5281    /// verified, hard upstream `sv-lang`=0.1.9 API gap) and `RepeatedEvent`
5282    /// (verified structurally unreachable at this dispatch point -- see that
5283    /// arm below) remain named [`LoweringError`]s.
5284    /// See [`Self::walk_stmt`]'s doc comment: `TimingControlKind` is also
5285    /// `#[non_exhaustive]` upstream, so this dispatch is likewise an
5286    /// `if`/`else-if` chain (pinned by
5287    /// `determinism::kind_guard::timing_control_kind_count_matches_pinned_
5288    /// sv_lang_version`, `TimingControlKind::COUNT == 10`), not a literal
5289    /// `match`.
5290    fn walk_timed(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
5291        let tc = stmt
5292            .timing_control()
5293            .ok_or_else(|| LoweringError::malformed("Timed statement with no timing control"))?;
5294        if !class_event_wait_terms(&tc)?.is_empty() && class_event_wait_is_mixed(&tc) {
5295            // `@(ce or sig ...)` (LRM 1800-2023 9.4.2.1, 15.5): the process
5296            // waits on a class event (a runtime queue) and on signals (the
5297            // sensitivity table) at once. The leaf statement queues it on the
5298            // class events without blocking, the suspend is the signal
5299            // wait, and whichever wakes it first wins: the resume block leaves
5300            // the class queues (`ClassEventLeave`), and a class trigger
5301            // desensitises the signal side (`Engine::apply_sync_wake`).
5302            let guarded = stmt
5303                .body()
5304                .ok_or_else(|| LoweringError::malformed("Timed statement with no guarded body"))?;
5305            self.skip_class_event_terms = true;
5306            let point = self.suspend_point(tc, None);
5307            self.skip_class_event_terms = false;
5308            let point = point?;
5309            self.push(cur, stmt);
5310            let resume = self.alloc_block();
5311            self.blocks[resume.0 as usize]
5312                .bindings
5313                .push(ArgBinding::ClassEventLeave);
5314            self.close(
5315                cur,
5316                Terminator::Suspend {
5317                    point,
5318                    resume_blk: resume,
5319                },
5320            );
5321            return self.walk_stmt(guarded, resume);
5322        }
5323        if !class_event_wait_terms(&tc)?.is_empty() {
5324            // `@e` on a class event (LRM 1800-2023 15.5): the statement is the
5325            // leaf that queues the process on the event's runtime object; the
5326            // suspend after it lets the scheduler apply the block request,
5327            // like a blocking `semaphore.get`. Codegen lowers only the wait of
5328            // this leaf (the guarded body follows in the resumed block).
5329            let guarded = stmt
5330                .body()
5331                .ok_or_else(|| LoweringError::malformed("Timed statement with no guarded body"))?;
5332            if let Some(iff) = extract_iff(&tc) {
5333                // `@(e iff c)` (LRM 1800-2023 9.4.2.3): the event wakes the
5334                // process, which continues only if `c` holds then; otherwise
5335                // it waits for the event again. A loop around the plain wait.
5336                let header = self.alloc_block();
5337                let body = self.alloc_block();
5338                let exit = self.alloc_block();
5339                let resume = self.alloc_block();
5340                self.close(
5341                    cur,
5342                    Terminator::Loop {
5343                        header,
5344                        body,
5345                        exit,
5346                        entry: header,
5347                    },
5348                );
5349                self.close(header, Terminator::Fallthrough(body));
5350                self.push(body, stmt);
5351                self.close(
5352                    body,
5353                    Terminator::Suspend {
5354                        point: SuspendPoint::ByRequest,
5355                        resume_blk: resume,
5356                    },
5357                );
5358                self.close(
5359                    resume,
5360                    Terminator::Branch {
5361                        cond: iff,
5362                        then_blk: exit,
5363                        else_blk: header,
5364                    },
5365                );
5366                return self.walk_stmt(guarded, exit);
5367            }
5368            self.push(cur, stmt);
5369            let resume = self.alloc_block();
5370            self.close(
5371                cur,
5372                Terminator::Suspend {
5373                    point: SuspendPoint::ByRequest,
5374                    resume_blk: resume,
5375                },
5376            );
5377            return self.walk_stmt(guarded, resume);
5378        }
5379        self.allow_dynamic_cycle = true;
5380        let point = self.suspend_point(tc, stmt.body());
5381        self.allow_dynamic_cycle = false;
5382        let point = point?;
5383        let guarded = stmt
5384            .body()
5385            .ok_or_else(|| LoweringError::malformed("Timed statement with no guarded body"))?;
5386        if let Some(count) = self.dynamic_cycle_count.take() {
5387            // `##(expr)` with a run-time count (LRM 14.11): `repeat (expr)`
5388            // around one `##1` wait, the count evaluated once on entry. A
5389            // count <= 0 waits for no clock event.
5390            let entry = self.alloc_block();
5391            self.close(cur, Terminator::Fallthrough(entry));
5392            let counter = self.alloc_repeat_counter();
5393            self.set_repeat_init(entry, counter, RepeatCount::Expr(count));
5394            let header = self.alloc_block();
5395            let body = self.alloc_block();
5396            let exit = self.alloc_block();
5397            let resume = self.alloc_block();
5398            self.close(
5399                entry,
5400                Terminator::Loop {
5401                    header,
5402                    body,
5403                    exit,
5404                    entry: header,
5405                },
5406            );
5407            self.close(
5408                header,
5409                Terminator::RepeatCheck {
5410                    counter,
5411                    then_blk: body,
5412                    else_blk: exit,
5413                },
5414            );
5415            self.close(
5416                body,
5417                Terminator::Suspend {
5418                    point,
5419                    resume_blk: resume,
5420                },
5421            );
5422            self.set_repeat_decrement(resume, counter);
5423            self.close(resume, Terminator::Fallthrough(header));
5424            return self.walk_stmt(guarded, exit);
5425        }
5426        let resume = self.alloc_block();
5427        self.close(
5428            cur,
5429            Terminator::Suspend {
5430                point,
5431                resume_blk: resume,
5432            },
5433        );
5434        self.walk_stmt(guarded, resume)
5435    }
5436
5437    /// `lhs = #d rhs;`, `lhs = @(e) rhs;`, `lhs = repeat (n) @(e) rhs;` (LRM
5438    /// 1800-2023 §10.4.1): the right-hand side is evaluated now into a
5439    /// compiler temporary ([`ArgBinding::Capture`]), the process waits, then
5440    /// the temporary is assigned ([`ArgBinding::AssignCaptured`]).
5441    fn walk_intra_assignment_timing(
5442        &mut self,
5443        assign: Expression<'d>,
5444        cur: BlockId,
5445    ) -> Result<BlockId, LoweringError> {
5446        let tc = assign
5447            .assignment_timing()
5448            .and_then(|n| n.as_timing_control())
5449            .ok_or_else(|| LoweringError::malformed("assignment timing is not a timing control"))?;
5450        let target = assign
5451            .left()
5452            .ok_or_else(|| LoweringError::malformed("assignment with no target"))?;
5453        let value = assign
5454            .right()
5455            .ok_or_else(|| LoweringError::malformed("assignment with no value"))?;
5456        let temp = self.captures.len() as u32;
5457        self.captures.push(target);
5458        // A fresh block: its bindings run before anything else in it, and
5459        // the capture must follow `cur`'s own statements.
5460        let capture = self.alloc_block();
5461        self.close(cur, Terminator::Fallthrough(capture));
5462        self.blocks[capture.0 as usize]
5463            .bindings
5464            .push(ArgBinding::Capture { temp, value });
5465        self.drive_clocking = drive_clocking_of(target);
5466        self.drive_via = drive_via_of(target);
5467        let after = if tc.kind() == TimingControlKind::RepeatedEvent {
5468            // `repeat (n) @(e)`: wait for `n` occurrences of `e` -- a counted
5469            // loop around one suspend (a count <= 0 waits for none).
5470            let count = first_expression_child(&tc).ok_or_else(|| {
5471                LoweringError::malformed("repeated event control with no count expression")
5472            })?;
5473            let event = tc
5474                .children()
5475                .into_iter()
5476                .find_map(|n| n.as_timing_control())
5477                .ok_or_else(|| {
5478                    LoweringError::malformed("repeated event control with no event control")
5479                })?;
5480            let point = self.suspend_point(event, None)?;
5481            let counter = self.alloc_repeat_counter();
5482            self.set_repeat_init(capture, counter, RepeatCount::Expr(count));
5483            let header = self.alloc_block();
5484            let body = self.alloc_block();
5485            let exit = self.alloc_block();
5486            let resume = self.alloc_block();
5487            self.close(
5488                capture,
5489                Terminator::Loop {
5490                    header,
5491                    body,
5492                    exit,
5493                    entry: header,
5494                },
5495            );
5496            self.close(
5497                header,
5498                Terminator::RepeatCheck {
5499                    counter,
5500                    then_blk: body,
5501                    else_blk: exit,
5502                },
5503            );
5504            self.close(
5505                body,
5506                Terminator::Suspend {
5507                    point,
5508                    resume_blk: resume,
5509                },
5510            );
5511            self.set_repeat_decrement(resume, counter);
5512            self.close(resume, Terminator::Fallthrough(header));
5513            exit
5514        } else {
5515            let point = self.suspend_point(tc, None)?;
5516            let resume = self.alloc_block();
5517            self.close(
5518                capture,
5519                Terminator::Suspend {
5520                    point,
5521                    resume_blk: resume,
5522                },
5523            );
5524            resume
5525        };
5526        self.drive_clocking = None;
5527        self.drive_via = None;
5528        self.blocks[after.0 as usize]
5529            .bindings
5530            .push(ArgBinding::AssignCaptured {
5531                temp,
5532                target,
5533                nba: false,
5534            });
5535        Ok(after)
5536    }
5537
5538    /// `lhs <= @(e) rhs;`, `lhs <= repeat (n) @(e) rhs;` (LRM 1800-2023
5539    /// §10.4.2, §9.4.5): the right-hand side (and the repeat count) are
5540    /// evaluated now into the compiler temporaries of this process, which
5541    /// goes straight on; a synthetic `join_none` branch, spawned here with
5542    /// copies of those temporaries ([`ProcessCfg::inherited`]), waits for
5543    /// the event control and then stages the nonblocking update.
5544    ///
5545    /// Deviation: the branch is a child of this process like any forked
5546    /// one, so a later `wait fork;` waits for the pending update and
5547    /// `disable fork;` cancels it.
5548    fn walk_nba_event(
5549        &mut self,
5550        stmt: Statement<'d>,
5551        assign: Expression<'d>,
5552        cur: BlockId,
5553    ) -> Result<BlockId, LoweringError> {
5554        let tc = assign
5555            .assignment_timing()
5556            .and_then(|n| n.as_timing_control())
5557            .ok_or_else(|| LoweringError::malformed("assignment timing is not a timing control"))?;
5558        let target = assign
5559            .left()
5560            .ok_or_else(|| LoweringError::malformed("assignment with no target"))?;
5561        let value = assign
5562            .right()
5563            .ok_or_else(|| LoweringError::malformed("assignment with no value"))?;
5564        let temp = self.captures.len() as u32;
5565        self.captures.push(target);
5566        let capture = self.alloc_block();
5567        self.close(cur, Terminator::Fallthrough(capture));
5568        self.blocks[capture.0 as usize]
5569            .bindings
5570            .push(ArgBinding::Capture { temp, value });
5571
5572        // The branch: its own builder, with this walk's context.
5573        let mut b = CfgBuilder::new();
5574        b.this_stack.extend(self.this_stack.last().copied());
5575        b.ref_stack = self.ref_stack.clone();
5576        b.global_clock = self.global_clock;
5577        b.virtual_tasks = self.virtual_tasks.clone();
5578        b.fork_branch_names = self.fork_branch_names.clone();
5579        b.ancestor_blocks = self.ancestor_blocks.clone();
5580        b.default_clocking = self.default_clocking;
5581        b.drive_clocking = drive_clocking_of(target);
5582        b.drive_via = drive_via_of(target);
5583        b.checker_clock = self.checker_clock;
5584        b.captures.push(target);
5585        let mut inherited = vec![InheritedSlot::Temp {
5586            parent: temp,
5587            child: 0,
5588        }];
5589        let entry = b.alloc_block();
5590        let event_roots = vec![target.as_sem_node(), tc.as_sem_node()];
5591        // `lhs <= ##(n) v` with a run-time count (LRM 14.11, 14.16): the count
5592        // is evaluated here and the branch loops one `##1` wait that often,
5593        // like `repeat (n) @(clk)`.
5594        let mut dyn_cycle: Option<(Expression<'d>, SuspendPoint)> = None;
5595        let mut const_point: Option<SuspendPoint> = None;
5596        if tc.kind() == TimingControlKind::CycleDelay {
5597            b.allow_dynamic_cycle = true;
5598            let point = b.suspend_point(tc, None);
5599            b.allow_dynamic_cycle = false;
5600            let point = point?;
5601            match b.dynamic_cycle_count.take() {
5602                Some(count) => dyn_cycle = Some((count, point)),
5603                None => const_point = Some(point),
5604            }
5605        }
5606        let after = if dyn_cycle.is_some() || tc.kind() == TimingControlKind::RepeatedEvent {
5607            let (count, point) = if let Some(d) = dyn_cycle.take() {
5608                d
5609            } else {
5610                let count = first_expression_child(&tc).ok_or_else(|| {
5611                    LoweringError::malformed("repeated event control with no count expression")
5612                })?;
5613                let event = tc
5614                    .children()
5615                    .into_iter()
5616                    .find_map(|n| n.as_timing_control())
5617                    .ok_or_else(|| {
5618                        LoweringError::malformed("repeated event control with no event control")
5619                    })?;
5620                (count, b.suspend_point(event, None)?)
5621            };
5622            // The count is evaluated here, in this process.
5623            let parent_counter = self.alloc_repeat_counter();
5624            self.set_repeat_init(capture, parent_counter, RepeatCount::Expr(count));
5625            let counter = b.alloc_repeat_counter();
5626            inherited.push(InheritedSlot::Counter {
5627                parent: parent_counter,
5628                child: counter,
5629            });
5630            b.set_repeat_init(entry, counter, RepeatCount::Inherited(count));
5631            let header = b.alloc_block();
5632            let body = b.alloc_block();
5633            let exit = b.alloc_block();
5634            let resume = b.alloc_block();
5635            b.close(
5636                entry,
5637                Terminator::Loop {
5638                    header,
5639                    body,
5640                    exit,
5641                    entry: header,
5642                },
5643            );
5644            b.close(
5645                header,
5646                Terminator::RepeatCheck {
5647                    counter,
5648                    then_blk: body,
5649                    else_blk: exit,
5650                },
5651            );
5652            b.close(
5653                body,
5654                Terminator::Suspend {
5655                    point,
5656                    resume_blk: resume,
5657                },
5658            );
5659            b.set_repeat_decrement(resume, counter);
5660            b.close(resume, Terminator::Fallthrough(header));
5661            exit
5662        } else {
5663            let point = match const_point.take() {
5664                Some(p) => p,
5665                None => b.suspend_point(tc, None)?,
5666            };
5667            let resume = b.alloc_block();
5668            b.close(
5669                entry,
5670                Terminator::Suspend {
5671                    point,
5672                    resume_blk: resume,
5673                },
5674            );
5675            resume
5676        };
5677        b.blocks[after.0 as usize]
5678            .bindings
5679            .push(ArgBinding::AssignCaptured {
5680                temp: 0,
5681                target,
5682                nba: true,
5683            });
5684        b.close(after, Terminator::End { loop_back: None });
5685        let inlined_locals = std::mem::take(&mut b.inlined_locals);
5686        let captures = std::mem::take(&mut b.captures);
5687        let ref_bindings = b.take_ref_bindings();
5688        let disable_scopes = b.take_disable_scopes();
5689        let disable_broadcast = std::mem::take(&mut b.disable_broadcast);
5690        let fork_branch_names = std::mem::take(&mut b.fork_branch_names);
5691        let mut branch = ProcessCfg {
5692            blocks: b.finish(),
5693            entry,
5694            body: stmt,
5695            inlined_locals,
5696            captures,
5697            captured: Vec::new(),
5698            ref_bindings,
5699            disable_names: Vec::new(),
5700            fork_branch_names,
5701            disable_scopes,
5702            disable_broadcast,
5703            inherited,
5704            capture_types: Vec::new(),
5705            recursion: Recursion::None,
5706            share_id: None,
5707        };
5708        // It shares the variables its event control and target name.
5709        branch.captured = captured_in(event_roots);
5710        branch
5711            .captured
5712            .retain(|c| !self.ref_stack.iter().any(|(f, _)| f.uid() == c.uid()));
5713        if let Some(t) = self.this_stack.last()
5714            && !branch.captured.iter().any(|c| c.uid() == t.uid())
5715        {
5716            branch.captured.push(*t);
5717        }
5718        let join_blk = self.alloc_block();
5719        self.close(
5720            capture,
5721            Terminator::ForkJoin {
5722                kind: StatementBlockKind::JoinNone,
5723                branches: vec![branch],
5724                join_blk,
5725            },
5726        );
5727        Ok(join_blk)
5728    }
5729
5730    /// A signal event's `(signal, edge)` with a checker's `$inferred_clock` (17.4)
5731    /// replaced by the clocking event of the instantiation context. Any other
5732    /// event is returned as written. A checker instantiated where no clock is
5733    /// inferred is a loud error, never a wait on nothing.
5734    fn resolve_inferred_clock(
5735        &self,
5736        signal: Expression<'d>,
5737        edge: EdgeKind,
5738    ) -> Result<InferredClockEvent<'d>, LoweringError> {
5739        if is_global_clock_call(&signal) {
5740            // `@$global_clock` (14.14, 17.7's counter_model): the global
5741            // clocking's event.
5742            return self.global_clock.ok_or_else(|| {
5743                LoweringError::unsupported(
5744                    "`@$global_clock` as a procedural event control needs a `global clocking` \
5745                     declared in the process's instance or an enclosing one (LRM 14.14); none \
5746                     is in scope",
5747                )
5748            });
5749        }
5750        if !is_inferred_clock_call(&signal) {
5751            return Ok((signal, edge, None));
5752        }
5753        self.checker_clock.ok_or_else(|| {
5754            LoweringError::unsupported(
5755                "`$inferred_clock` (LRM 17.4) as the event control of a checker procedure: the \
5756                 checker is instantiated where no default clocking applies (14.12), so no \
5757                 clock is inferred",
5758            )
5759        })
5760    }
5761
5762    /// The suspend point a timing control waits on; `guarded` is the
5763    /// statement an `@*` implicit event control guards (its reads are the
5764    /// sensitivity).
5765    fn suspend_point(
5766        &mut self,
5767        tc: TimingControl<'d>,
5768        guarded: Option<Statement<'d>>,
5769    ) -> Result<SuspendPoint<'d>, LoweringError> {
5770        let tc_kind = tc.kind();
5771
5772        let point = if tc_kind == TimingControlKind::Invalid {
5773            return Err(LoweringError::malformed("Invalid timing control kind"));
5774        } else if tc_kind == TimingControlKind::Delay {
5775            let expr = first_expression_child(&tc).ok_or_else(|| {
5776                LoweringError::malformed("Delay timing control with no delay expression")
5777            })?;
5778            SuspendPoint::Delay(expr, self.frame_stack.last().map(|f| f.0))
5779        } else if tc_kind == TimingControlKind::SignalEvent {
5780            let edge = tc.edge();
5781            let signal = first_expression_child(&tc).ok_or_else(|| {
5782                LoweringError::malformed("SignalEvent timing control with no signal expression")
5783            })?;
5784            reject_chandle_event(&signal)?;
5785            let iff = extract_iff(&tc);
5786            let (signal, edge, inferred_iff) = self.resolve_inferred_clock(signal, edge)?;
5787            // `@clock iff c` on an inferred clock that itself has an `iff`
5788            // needs both guards (a conjunction, LRM 17.4): a one-member
5789            // event list carries them; `Edge` has room for one only.
5790            if let (Some(a), Some(b)) = (iff, inferred_iff) {
5791                if edge == EdgeKind::PosEdge || edge == EdgeKind::NegEdge {
5792                    self.note_edge_signal(signal, edge);
5793                }
5794                return Ok(SuspendPoint::EdgeList(
5795                    vec![(signal, edge)],
5796                    vec![vec![a, b]],
5797                ));
5798            }
5799            let iff = iff.or(inferred_iff);
5800            if edge != EdgeKind::PosEdge && edge != EdgeKind::NegEdge {
5801                // `@(a)` (any change) / `@(edge a)`: a one-entry event list,
5802                // resolved through the program's wait table like `@(a or b)`.
5803                // With `iff` (LRM 1800-2023 9.4.2.3: `@(e iff c)` waits for a
5804                // change of `e`, or for the event, and then for `c`) it is a
5805                // single `Edge` term carrying its guard.
5806                if iff.is_some() {
5807                    SuspendPoint::Edge { signal, edge, iff }
5808                } else {
5809                    SuspendPoint::EdgeList(vec![(signal, edge)], vec![Vec::new()])
5810                }
5811            } else {
5812                self.note_edge_signal(signal, edge);
5813                SuspendPoint::Edge { signal, edge, iff }
5814            }
5815        } else if tc_kind == TimingControlKind::EventList {
5816            // Task 1: `@(a or b or c)` -- wake on any member. Each child is
5817            // itself a nested `TimingControl` (in practice always
5818            // `SignalEvent`, per the grammar). A member's `iff` (LRM
5819            // 1800-2023 9.4.2.3) rides along per member in the point's
5820            // second field.
5821            let mut entries = Vec::new();
5822            let mut iffs = Vec::new();
5823            for child in tc.children() {
5824                let sub_tc = child.as_timing_control().ok_or_else(|| {
5825                    LoweringError::malformed(
5826                        "EventList timing control with a non-timing-control child",
5827                    )
5828                })?;
5829                let edge = sub_tc.edge();
5830                let signal = first_expression_child(&sub_tc).ok_or_else(|| {
5831                    LoweringError::malformed(
5832                        "EventList member timing control with no signal expression",
5833                    )
5834                })?;
5835                if self.skip_class_event_terms && is_class_event_expr(&signal) {
5836                    continue;
5837                }
5838                reject_chandle_event(&signal)?;
5839                let iff = extract_iff(&sub_tc);
5840                let (signal, edge, inferred_iff) = self.resolve_inferred_clock(signal, edge)?;
5841                if edge == EdgeKind::PosEdge || edge == EdgeKind::NegEdge {
5842                    self.note_edge_signal(signal, edge);
5843                }
5844                entries.push((signal, edge));
5845                iffs.push(iff.into_iter().chain(inferred_iff).collect());
5846            }
5847            if entries.is_empty() {
5848                return Err(LoweringError::malformed(
5849                    "EventList timing control with no member event controls",
5850                ));
5851            }
5852            SuspendPoint::EdgeList(entries, iffs)
5853        } else if tc_kind == TimingControlKind::CycleDelay {
5854            // Task 1 shape, Task 6 (CG-171) revised semantics -- see
5855            // `SuspendPoint::CycleDelay`'s own doc comment for the full
5856            // reasoning. Still a named `LoweringError`, never a silent
5857            // "assume 1 clock unit", when there is no unambiguous single
5858            // clock in scope.
5859            let count_expr = first_expression_child(&tc).ok_or_else(|| {
5860                LoweringError::malformed("CycleDelay timing control with no count expression")
5861            })?;
5862            let constant = count_expr
5863                .constant_value()
5864                .and_then(|v| v.as_i64())
5865                .and_then(|v| u32::try_from(v).ok());
5866            let count = match constant {
5867                Some(c) => c,
5868                None if self.allow_dynamic_cycle => {
5869                    self.dynamic_cycle_count = Some(count_expr);
5870                    1
5871                }
5872                None => {
5873                    return Err(LoweringError::unsupported(
5874                        "a non-constant cycle delay `##(expr)` in an intra-assignment timing \
5875                         control (LRM 14.11 allows a run-time count only as a statement \
5876                         delay; this simulator needs a compile-time constant here)",
5877                    ));
5878                }
5879            };
5880            // The default clocking block's event (LRM 1800-2023 §14.11),
5881            // else the process's own inferred clock.
5882            let (clock, edge) = match (
5883                self.drive_clocking.or(self.default_clocking),
5884                self.inferred_clock,
5885            ) {
5886                (Some(cb), _) => (cb, EdgeKind::BothEdges),
5887                (None, Some(Some((signal, edge)))) => (
5888                    signal.referenced_symbol().ok_or_else(|| {
5889                        LoweringError::unsupported("CycleDelay (`##N`) clock with no symbol")
5890                    })?,
5891                    edge,
5892                ),
5893                _ => {
5894                    return Err(LoweringError::unsupported(
5895                        "CycleDelay (`##N`) with no unambiguous single inferred clock in scope \
5896                         (no enclosing posedge/negedge edge, or more than one distinct clock \
5897                         signal/edge seen) -- ARCHITECTURE §6.5's own \"ClockDomain the scheduler \
5898                         needs\" requirement",
5899                    ));
5900                }
5901            };
5902            let via = if self.drive_clocking.is_some() {
5903                self.drive_via
5904            } else {
5905                None
5906            };
5907            SuspendPoint::CycleDelay {
5908                count,
5909                clock,
5910                edge,
5911                via,
5912            }
5913        } else if tc_kind == TimingControlKind::ImplicitEvent {
5914            // Task 6 (CG-081): `@*`/`@(*)`. See `SuspendPoint::ImplicitComb`'s
5915            // own doc comment for why the read-set is captured here (AST-only,
5916            // no live Analysis handle reaches this crate).
5917            let body_stmt = guarded.ok_or_else(|| {
5918                LoweringError::unsupported(
5919                    "an `@*` event control with no guarded statement (an intra-assignment `@*`)",
5920                )
5921            })?;
5922            SuspendPoint::ImplicitComb(implicit_comb_read_set(&body_stmt))
5923        } else if tc_kind == TimingControlKind::RepeatedEvent {
5924            // Task 6 (CG-081): `repeat(count) @(...)`. **Verified,
5925            // structurally unreachable at this dispatch point, never a scope
5926            // choice** -- empirically probed against the pinned
5927            // `sv-lang`=0.1.9 parser (a throwaway test compiling `initial q
5928            // <= repeat(3) @(posedge clk) 1;` and dumping the AST): a
5929            // `RepeatedEventControl` node is NEVER the outer `TimingControl`
5930            // of a `StatementKind::Timed` node at all. Two independent
5931            // parser facts combine to make this so: (1) `Parser::
5932            // parseStatement`'s own dispatch switch (`Parser_statements.cpp`)
5933            // sends a leading `repeat` keyword straight to
5934            // `parseLoopStatement` (producing `StatementKind::RepeatLoop`,
5935            // already handled by `walk_stmt`/`is_ordinary_statement_kind`) --
5936            // it never reaches `parseTimingControl()` at all, so
5937            // `repeat(3) @(posedge clk) q = 1;` used directly as a statement
5938            // parses as "run `@(posedge clk) q = 1;` 3 times", not "wait for
5939            // clk's 3rd posedge, then run `q = 1;` once". (2) The ONLY
5940            // grammar position `parseTimingControl()`'s own `RepeatKeyword`
5941            // case (`Parser_expressions.cpp`) is ever reached from is
5942            // `parseSubExpression`'s `isPossibleDelayOrEventControl` check --
5943            // i.e. `RepeatedEventControl` is only ever constructible as an
5944            // INTRA-ASSIGNMENT timing control embedded in an `Assignment`
5945            // expression's own `.children()` (confirmed empirically: `q <=
5946            // repeat(3) @(posedge clk) 1;` parses as one
5947            // `StatementKind::ExpressionStatement` whose `Assignment`
5948            // expression's `children()` is `[TimingControl(RepeatedEvent),
5949            // NamedValue, Conversion]` -- verified against
5950            // `AssignmentExpression::visitExprs` in the vendored slang C++,
5951            // `if (timingControl) timingControl->visit(visitor); left()...;
5952            // right()...;`), never a `Statement::timing_control()` at all.
5953            // The intra-assignment form is lowered by
5954            // `walk_intra_assignment_timing` (the right-hand side captured in
5955            // a `Temporary` frame slot before the counted wait), which asks
5956            // this function only for the inner event control's suspend
5957            // point -- so reaching this arm means a malformed AST.
5958            return Err(LoweringError::unsupported(
5959                "RepeatedEvent timing control (`repeat(n) @(...)`) can never be the outer \
5960                 TimingControlKind of a Timed statement -- `repeat` at statement-position \
5961                 always parses as StatementKind::RepeatLoop instead, and RepeatedEventControl \
5962                 is only ever producible as an intra-assignment timing control, which \
5963                 walk_intra_assignment_timing lowers",
5964            ));
5965        } else if tc_kind == TimingControlKind::OneStepDelay {
5966            // Task 6 (CG-081): `#0` / the built-in `1step` delay -- see
5967            // `SuspendPoint::OneStep`'s own doc comment.
5968            SuspendPoint::OneStep
5969        } else if tc_kind == TimingControlKind::BlockEventList {
5970            // LRM 19.3: a block event list `@@(begin f)` is only ever a
5971            // covergroup's sampling event (lowered by `covergroup` block-event
5972            // registration); slang rejects it as a procedural timing control
5973            // at parse ("expected statement"), so this arm is unreachable.
5974            return Err(LoweringError::unsupported(
5975                "a block event list `@@(...)` as a procedural timing control (LRM 19.3: \
5976                 only a covergroup sampling event)",
5977            ));
5978        } else if tc_kind == TimingControlKind::Delay3 {
5979            // Delay3Control (`#(d1,d2,d3)`) is gate-primitive rise/fall/
5980            // turn-off delay syntax (LRM 28.14) -- not applicable to a
5981            // procedural `Timed` statement's suspend point at all (CG-081's
5982            // own 8-kind enumeration excludes it), so this stays a named
5983            // `LoweringError` unconditionally, not a gap this task closes.
5984            return Err(LoweringError::unsupported(
5985                "Delay3 timing control (gate-primitive rise/fall/turn-off delay syntax -- not \
5986                 applicable to a procedural statement's suspend point)",
5987            ));
5988        } else {
5989            // Unreachable as long as `determinism::kind_guard`'s pinned
5990            // `TimingControlKind::COUNT == 10` still holds -- see
5991            // `walk_stmt`'s identical reasoning for `StatementKind`.
5992            return Err(LoweringError::malformed(format!(
5993                "unrecognized TimingControlKind {tc_kind:?} (sv-lang-kinds version drift -- \
5994                 see determinism::kind_guard's TimingControlKind pin)"
5995            )));
5996        };
5997        Ok(point)
5998    }
5999
6000    /// Task 3 (PR-006's shape from Task 1, real semantics from CG-169/
6001    /// CG-115 here): `wait(expr) stmt;` (LRM 9.4.3). Unlike every other
6002    /// `Timed`/`Wait`-family suspend point, this one does NOT
6003    /// unconditionally close `cur` with `Terminator::Suspend` -- LRM 9.4.3 is
6004    /// an *immediate check with a conditional suspend*: if `expr` is already
6005    /// true the moment execution reaches it, there is NO suspend/resume
6006    /// round-trip at all (straight fallthrough into the guarded body, same
6007    /// call); only a false evaluation actually suspends. This is
6008    /// structurally the same "sometimes there's no suspend point at all"
6009    /// shape `docs/design/process.md` §2 calls out by name, and it is
6010    /// represented here exactly like any other runtime-decided branch: a
6011    /// `Terminator::Branch` whose `then_blk` is the guarded body's own entry
6012    /// block (`resume`) and whose `else_blk` is a fresh block that alone
6013    /// carries the real `Terminator::Suspend{point: SuspendPoint::Level(cond),
6014    /// resume_blk: resume}` -- `resume` is deliberately the SAME `BlockId` on
6015    /// both edges, so there genuinely is a suspend-free path from `cur` to
6016    /// the guarded body in the built `ProcessCfg` (a purely structural,
6017    /// compile-time fact -- codegen decides which edge is taken at
6018    /// simulation runtime by reusing the exact same generic branch-condition
6019    /// lowering `if`/`while` already use, `docs/design/process.md`'s own
6020    /// `is_definitely_true` reduction list explicitly includes "wait()
6021    /// condition" alongside "if"/"while" for this reason, PR-069's
6022    /// `Terminator`/`SuspendPoint` "cross-check against
6023    /// contains_suspend_trigger" extension policy). `resume`'s own entry is
6024    /// therefore executed either way (already-true fallthrough, or a genuine
6025    /// resume from the scheduler after the `Suspend` fires) -- the
6026    /// resume-prologue re-check that makes a spurious wake re-suspend
6027    /// without running the body is CG-115's own job, entirely on the
6028    /// codegen side (`sv-sim-codegen::process_lower::compile_iff_guard`);
6029    /// this crate never evaluates `expr` itself (no runtime state reaches
6030    /// this AST-only builder).
6031    fn walk_wait(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
6032        let cond = stmt.condition().ok_or_else(|| {
6033            LoweringError::malformed("Wait statement with no condition expression")
6034        })?;
6035        let resume = self.alloc_block();
6036        let suspend_blk = self.alloc_block();
6037        self.close(
6038            cur,
6039            Terminator::Branch {
6040                cond,
6041                then_blk: resume,
6042                else_blk: suspend_blk,
6043            },
6044        );
6045        self.close(
6046            suspend_blk,
6047            Terminator::Suspend {
6048                point: SuspendPoint::Level(cond),
6049                resume_blk: resume,
6050            },
6051        );
6052        let guarded = stmt
6053            .body()
6054            .ok_or_else(|| LoweringError::malformed("Wait statement with no guarded body"))?;
6055        self.walk_stmt(guarded, resume)
6056    }
6057
6058    /// `-> ev;` / `->> ev;` / `->> #d ev;` / `->> @(ctl) ev;` (LRM 1800-2023
6059    /// 15.5.1, 15.5.2). An `EventTrigger` with no timing, or with a `#d`
6060    /// delay, never suspends the issuer -- it is one ordinary leaf (codegen
6061    /// stages the delayed trigger as a nonblocking update). One with an event
6062    /// control (`->> @(ctl) ev;`) forks a detached one-shot process that
6063    /// waits on `ctl` and then triggers, so the issuer continues at once
6064    /// (`spawn_detached_trigger`).
6065    fn walk_event_trigger(
6066        &mut self,
6067        stmt: Statement<'d>,
6068        cur: BlockId,
6069    ) -> Result<BlockId, LoweringError> {
6070        let detached = std::mem::take(&mut self.detached_trigger);
6071        let Some(timing_node) = stmt.timing() else {
6072            self.push(cur, stmt);
6073            return Ok(cur);
6074        };
6075        let tc = timing_node.as_timing_control().ok_or_else(|| {
6076            LoweringError::malformed("EventTrigger timing is not a TimingControl node")
6077        })?;
6078        if tc.kind() == TimingControlKind::Delay {
6079            // `->> #d e;` never blocks the issuer: the trigger is staged as a
6080            // nonblocking update `d` ticks out (codegen reads the delay).
6081            self.push(cur, stmt);
6082            return Ok(cur);
6083        }
6084        if !detached {
6085            // `->> @(ctl) e;`: the issuer continues at once; a detached
6086            // process waits for `ctl`, then stages the trigger.
6087            return self.spawn_detached_trigger(stmt, cur, ProcessLoopKind::OneShot);
6088        }
6089        let point = self.suspend_point(tc, None)?;
6090        let resume = self.alloc_block();
6091        self.close(
6092            cur,
6093            Terminator::Suspend {
6094                point,
6095                resume_blk: resume,
6096            },
6097        );
6098        self.push(resume, stmt);
6099        Ok(resume)
6100    }
6101
6102    /// Forks the detached one-shot process of `->> @(ctl) e;` (see
6103    /// [`Self::detached_trigger`]) and continues the issuer past it.
6104    fn spawn_detached_trigger(
6105        &mut self,
6106        stmt: Statement<'d>,
6107        cur: BlockId,
6108        loop_kind: ProcessLoopKind,
6109    ) -> Result<BlockId, LoweringError> {
6110        let mut branch = build_cfg_in(
6111            &stmt,
6112            loop_kind,
6113            None,
6114            self.default_clocking,
6115            None,
6116            self.checker_clock,
6117            Inherited {
6118                this_var: self.this_stack.last().copied(),
6119                ref_aliases: self.ref_stack.clone(),
6120                global_clock: self.global_clock,
6121                detached_trigger: true,
6122                forced_extern: None,
6123                rec_outer: self.rec_outer.clone(),
6124                fork_memo: Some(self.fork_memo.clone()),
6125                rec: None,
6126                rec_up: Vec::new(),
6127                known_fork_labels: Vec::new(),
6128                ancestor_blocks: Vec::new(),
6129            },
6130            self.virtual_tasks.clone(),
6131        )?;
6132        branch.captured = captured_vars(&branch);
6133        branch
6134            .captured
6135            .retain(|c| !self.ref_stack.iter().any(|(f, _)| f.uid() == c.uid()));
6136        if let Some(t) = self.this_stack.last()
6137            && !branch.captured.iter().any(|c| c.uid() == t.uid())
6138        {
6139            branch.captured.push(*t);
6140        }
6141        let join_blk = self.alloc_block();
6142        self.close(
6143            cur,
6144            Terminator::ForkJoin {
6145                kind: StatementBlockKind::JoinNone,
6146                branches: vec![branch],
6147                join_blk,
6148            },
6149        );
6150        Ok(join_blk)
6151    }
6152
6153    /// `$async$and$array(mem, in, out);` and its seven siblings (LRM 1800-2023
6154    /// 20.16.1): the logic array is evaluated at the call, and again whenever
6155    /// an input term or any word of the personality memory changes. The
6156    /// issuer evaluates once (the call is an ordinary leaf) and forks a
6157    /// detached looping process that evaluates, waits on the operands' read
6158    /// set, and re-runs the same statement on each wake (the
6159    /// `detached_trigger` flag marks that process's own copy, so it does not
6160    /// fork again).
6161    fn walk_async_pla(
6162        &mut self,
6163        stmt: Statement<'d>,
6164        cur: BlockId,
6165    ) -> Result<BlockId, LoweringError> {
6166        let detached = std::mem::take(&mut self.detached_trigger);
6167        if !detached {
6168            self.push(cur, stmt);
6169            return self.spawn_detached_trigger(stmt, cur, ProcessLoopKind::Looping);
6170        }
6171        // Evaluate first, then wait: an input that changed between the fork
6172        // and this process's first run (same time step) is not missed.
6173        let reads = async_pla_read_set(&stmt);
6174        let resume = self.alloc_block();
6175        self.push(cur, stmt);
6176        self.close(
6177            cur,
6178            Terminator::Suspend {
6179                point: SuspendPoint::ImplicitComb(reads),
6180                resume_blk: resume,
6181            },
6182        );
6183        Ok(resume)
6184    }
6185
6186    /// A call statement `stmt` to the interface `extern` method `targets`
6187    /// (LRM 25.7), when the call needs the CFG: it runs the implementation
6188    /// inline (one), or as the branches of a `fork`...`join` (an `extern
6189    /// forkjoin` task with several, 25.7.5), or reports the run-time error of
6190    /// a forkjoin task nothing implements and returns with no effect.
6191    /// `None` when the call is an ordinary leaf (every implementation runs
6192    /// without suspending: the code generator calls them in turn).
6193    fn walk_extern_call(
6194        &mut self,
6195        stmt: Statement<'d>,
6196        call: Expression<'d>,
6197        targets: &crate::netlist::itf::ExternTargets<'d>,
6198        cur: BlockId,
6199    ) -> Result<Option<BlockId>, LoweringError> {
6200        // One branch of an enclosing fork: this implementation only.
6201        if let Some(imp) = self.forced_extern.take() {
6202            return self.inline_task_call(call, imp, cur).map(Some);
6203        }
6204        if targets.impls.is_empty() {
6205            let join = self.alloc_block();
6206            self.close(
6207                cur,
6208                Terminator::Switch {
6209                    on: SwitchOn::NoTarget {
6210                        what: format!(
6211                            "no module connected to the interface implements extern forkjoin \
6212                             task `{}`; the call has no effect",
6213                            targets.proto.name()
6214                        ),
6215                    },
6216                    arms: Vec::new(),
6217                    default_blk: join,
6218                    report: Some(call.source_range().start),
6219                },
6220            );
6221            return Ok(Some(join));
6222        }
6223        if !targets.impls.iter().any(subroutine_can_suspend) {
6224            return Ok(None);
6225        }
6226        if targets.impls.len() == 1 {
6227            return self.inline_task_call(call, targets.impls[0], cur).map(Some);
6228        }
6229        // Every implementation as a branch of one `fork`...`join` (the call
6230        // returns when all have).
6231        let mut branches = Vec::with_capacity(targets.impls.len());
6232        for imp in &targets.impls {
6233            let mut branch = build_cfg_in(
6234                &stmt,
6235                ProcessLoopKind::OneShot,
6236                None,
6237                self.default_clocking,
6238                None,
6239                self.checker_clock,
6240                Inherited {
6241                    this_var: self.this_stack.last().copied(),
6242                    ref_aliases: self.ref_stack.clone(),
6243                    global_clock: self.global_clock,
6244                    detached_trigger: false,
6245                    forced_extern: Some(*imp),
6246                    rec_outer: self.rec_outer.clone(),
6247                    fork_memo: Some(self.fork_memo.clone()),
6248                    rec: None,
6249                    rec_up: Vec::new(),
6250                    known_fork_labels: self.fork_branch_names.clone(),
6251                    ancestor_blocks: self.ancestor_blocks.clone(),
6252                },
6253                self.virtual_tasks.clone(),
6254            )?;
6255            branch.captured = captured_vars(&branch);
6256            branch
6257                .captured
6258                .retain(|c| !self.ref_stack.iter().any(|(f, _)| f.uid() == c.uid()));
6259            if let Some(t) = self.this_stack.last()
6260                && !branch.captured.iter().any(|c| c.uid() == t.uid())
6261            {
6262                branch.captured.push(*t);
6263            }
6264            branches.push(branch);
6265        }
6266        let join_blk = self.alloc_block();
6267        self.close(
6268            cur,
6269            Terminator::ForkJoin {
6270                kind: StatementBlockKind::JoinAll,
6271                branches,
6272                join_blk,
6273            },
6274        );
6275        Ok(Some(join_blk))
6276    }
6277
6278    /// PR-045 (Task 1): `while (cond) body;`. `header` evaluates `cond`
6279    /// every iteration (re-checked, not cached) and branches to `body` or
6280    /// `exit`; `body`'s own exit closes the back edge to `header`; the real
6281    /// entry from `cur` is `header` (the condition is checked before the
6282    /// first iteration).
6283    fn walk_while_loop(
6284        &mut self,
6285        stmt: Statement<'d>,
6286        cur: BlockId,
6287    ) -> Result<BlockId, LoweringError> {
6288        let cond = stmt
6289            .condition()
6290            .ok_or_else(|| LoweringError::malformed("WhileLoop with no condition expression"))?;
6291        let body_stmt = stmt
6292            .body()
6293            .ok_or_else(|| LoweringError::malformed("WhileLoop with no body statement"))?;
6294
6295        let header = self.alloc_block();
6296        let body = self.alloc_block();
6297        let exit = self.alloc_block();
6298        // 38.36.1.2: before the condition, on every iteration.
6299        self.probe(header, stmt);
6300        self.close(
6301            cur,
6302            Terminator::Loop {
6303                header,
6304                body,
6305                exit,
6306                entry: header,
6307            },
6308        );
6309        self.close(
6310            header,
6311            Terminator::Branch {
6312                cond,
6313                then_blk: body,
6314                else_blk: exit,
6315            },
6316        );
6317        let body_exit = self.walk_loop_body(body_stmt, body, header, exit)?;
6318        self.close(body_exit, Terminator::Fallthrough(header));
6319        Ok(exit)
6320    }
6321
6322    /// PR-045 (Task 1): `do body; while (cond);`. Unlike `while`, `body`
6323    /// always runs once *before* any check -- the real entry from `cur` is
6324    /// `body` itself (`Terminator::Loop::entry`), and `header` (the
6325    /// post-body recheck, reached only via `body`'s own back edge) branches
6326    /// back to `body` or out to `exit`.
6327    fn walk_do_while_loop(
6328        &mut self,
6329        stmt: Statement<'d>,
6330        cur: BlockId,
6331    ) -> Result<BlockId, LoweringError> {
6332        let cond = stmt
6333            .condition()
6334            .ok_or_else(|| LoweringError::malformed("DoWhileLoop with no condition expression"))?;
6335        let body_stmt = stmt
6336            .body()
6337            .ok_or_else(|| LoweringError::malformed("DoWhileLoop with no body statement"))?;
6338
6339        let header = self.alloc_block();
6340        let body = self.alloc_block();
6341        let exit = self.alloc_block();
6342        self.close(
6343            cur,
6344            Terminator::Loop {
6345                header,
6346                body,
6347                exit,
6348                entry: body,
6349            },
6350        );
6351        self.close(
6352            header,
6353            Terminator::Branch {
6354                cond,
6355                then_blk: body,
6356                else_blk: exit,
6357            },
6358        );
6359        let body_exit = self.walk_loop_body(body_stmt, body, header, exit)?;
6360        self.close(body_exit, Terminator::Fallthrough(header));
6361        Ok(exit)
6362    }
6363
6364    /// PR-045 (Task 1): `forever body;`. No condition exists at all (LRM:
6365    /// unconditional) -- `header` unconditionally falls through to `body`,
6366    /// never to `exit`, so `exit` is allocated (every `Terminator::Loop`
6367    /// always names one, per PR-045's literal shape) but unreachable by any
6368    /// statically-analyzable path; only `disable`/`$finish` end a `forever`
6369    /// loop in practice, neither of which this function needs to wire.
6370    fn walk_forever_loop(
6371        &mut self,
6372        stmt: Statement<'d>,
6373        cur: BlockId,
6374    ) -> Result<BlockId, LoweringError> {
6375        let body_stmt = stmt
6376            .body()
6377            .ok_or_else(|| LoweringError::malformed("ForeverLoop with no body statement"))?;
6378
6379        let header = self.alloc_block();
6380        let body = self.alloc_block();
6381        let exit = self.alloc_block();
6382        // 38.36.1.2: when first encountered and on every iteration.
6383        self.probe(header, stmt);
6384        self.close(
6385            cur,
6386            Terminator::Loop {
6387                header,
6388                body,
6389                exit,
6390                entry: header,
6391            },
6392        );
6393        self.close(header, Terminator::Fallthrough(body));
6394        let body_exit = self.walk_loop_body(body_stmt, body, header, exit)?;
6395        self.close(body_exit, Terminator::Fallthrough(header));
6396        Ok(exit)
6397    }
6398
6399    /// PR-045 (Task 1): `for (init; cond; step) body;`. The declaration-form
6400    /// initializer (`for (int i = 0; ...)`) is a separate `VariableDeclaration`
6401    /// statement slang hoists *before* this `ForLoop` node in an implicit
6402    /// wrapper `Block` -- already handled for free by the ordinary `List`/
6403    /// `Block` walk, so this function never sees it. The pre-declared-var
6404    /// form (`for (i = 0; ...)`, `for_loop_initializers()` non-empty) is a
6405    /// named `LoweringError`: those initializers are bare `Expression`s, and
6406    /// (like `for_loop_steps()`, see `BasicBlock::step_exprs`'s doc comment)
6407    /// this crate has no way to run a bare pre-loop `Expression` as an
6408    /// ordinary leaf effect without fabricating a `Statement` node it does
6409    /// not have -- a real, if narrow, gap recorded in this task's
6410    /// deviations, not a silent drop. The condition (`stop expression`) is
6411    /// re-checked every iteration in `header`; the step expressions run via
6412    /// `step_exprs` on a dedicated `step` block between `body` and the back
6413    /// edge to `header`.
6414    fn walk_for_loop(
6415        &mut self,
6416        stmt: Statement<'d>,
6417        cur: BlockId,
6418    ) -> Result<BlockId, LoweringError> {
6419        // The classic Verilog form `for (i = 0; ...)` (pre-declared `i`):
6420        // its bare initializer expressions run as `step_exprs` on `cur`
6421        // itself -- after `cur`'s own statements, before the jump into
6422        // `header` -- the same bare-expression mechanism the loop's step
6423        // expressions use below.
6424        // 38.36.1.2: before any of the control expressions (the initializers
6425        // run as `cur`'s step expressions, after its statements) ...
6426        self.probe(cur, stmt);
6427        for init in stmt.for_loop_initializers() {
6428            self.push_step_expr(cur, init);
6429        }
6430        // `for (;;)`: an omitted stop expression is always true (LRM
6431        // 1800-2023 12.7.1), so `header` falls through to `body` unconditionally.
6432        let cond = stmt.condition();
6433        let body_stmt = stmt
6434            .body()
6435            .ok_or_else(|| LoweringError::malformed("ForLoop with no body statement"))?;
6436        let steps = stmt.for_loop_steps();
6437
6438        let header = self.alloc_block();
6439        let body = self.alloc_block();
6440        let exit = self.alloc_block();
6441        self.close(
6442            cur,
6443            Terminator::Loop {
6444                header,
6445                body,
6446                exit,
6447                entry: header,
6448            },
6449        );
6450        match cond {
6451            Some(cond) => self.close(
6452                header,
6453                Terminator::Branch {
6454                    cond,
6455                    then_blk: body,
6456                    else_blk: exit,
6457                },
6458            ),
6459            None => self.close(header, Terminator::Fallthrough(body)),
6460        }
6461        let step = self.alloc_block();
6462        let body_exit = self.walk_loop_body(body_stmt, body, step, exit)?;
6463        self.close(body_exit, Terminator::Fallthrough(step));
6464        // ... and on every iteration before the incremental statement.
6465        self.probe(step, stmt);
6466        for s in steps {
6467            self.push_step_expr(step, s);
6468        }
6469        self.close(step, Terminator::Fallthrough(header));
6470        Ok(exit)
6471    }
6472
6473    /// PR-045: `foreach (arr[i, j, ...]) body;`. A dimension's bound is a
6474    /// static `ConstantRange` (`Statement::foreach_loop_dims`), never an
6475    /// `Expression`, so each named dimension is a counted loop on the
6476    /// `repeat` machinery (`RepeatCount::Const` elements, a `RepeatCheck`
6477    /// header), its index variable set by `ArgBinding::Const`/`Step`. The
6478    /// body is lowered once. A dynamically-sized dimension (`range() ==
6479    /// None`) is a named `LoweringError`.
6480    fn walk_foreach_loop(
6481        &mut self,
6482        stmt: Statement<'d>,
6483        cur: BlockId,
6484    ) -> Result<BlockId, LoweringError> {
6485        // Over a queue, dynamic or associative array: a leaf statement
6486        // codegen lowers as one loop (`stmt_shared::lower_collection_
6487        // foreach`) when its body cannot suspend.
6488        let over_collection = stmt
6489            .expressions()
6490            .first()
6491            .and_then(|a| a.expr_type())
6492            .is_some_and(|t| crate::state::storage::is_collection(&t));
6493        // A suspending body over a collection nested in a collection or in a
6494        // fixed-size array (`int a[2][]`, `int q[$][$]`): `walk_nested_foreach`.
6495        let nested_dyn = stmt
6496            .foreach_loop_dims()
6497            .iter()
6498            .skip(usize::from(over_collection))
6499            .any(|d| d.range().is_none());
6500        // A `return` in the body of an inlined task has to jump out of the
6501        // CFG's task body (LRM 1800-2023 13.4.1), which a leaf loop cannot do.
6502        let returns = escapes(stmt.as_sem_node(), false, !self.return_targets.is_empty());
6503        if nested_dyn && (contains_suspend_trigger(stmt.as_sem_node()) || returns) {
6504            return self.walk_nested_foreach(stmt, cur);
6505        }
6506        if over_collection {
6507            if contains_suspend_trigger(stmt.as_sem_node()) || returns {
6508                return self.walk_collection_foreach(stmt, cur);
6509            }
6510            self.push(cur, stmt);
6511            return Ok(cur);
6512        }
6513        // A fixed-size array of queues / dynamic arrays (`int a[3][]`): one
6514        // leaf loop when the body cannot suspend (`stmt_shared::
6515        // lower_fixed_outer_foreach`).
6516        if stmt.foreach_loop_dims().iter().any(|d| d.range().is_none())
6517            && !contains_suspend_trigger(stmt.as_sem_node())
6518            && !returns
6519        {
6520            self.push(cur, stmt);
6521            return Ok(cur);
6522        }
6523        let dims = stmt.foreach_loop_dims();
6524        let body_stmt = stmt
6525            .body()
6526            .ok_or_else(|| LoweringError::malformed("ForeachLoop with no body statement"))?;
6527        // One counted loop per named dimension, outermost first; a dimension
6528        // whose loop variable is omitted (`foreach (a[i, ])`) is not
6529        // iterated (LRM 1800-2023 §12.7.3).
6530        let mut loops = Vec::new();
6531        for d in &dims {
6532            let Some(var) = d.loop_var() else {
6533                continue;
6534            };
6535            let range = d.range().ok_or_else(|| {
6536                LoweringError::unsupported(
6537                    "foreach over a dynamically-sized dimension (a dynamic array, queue or \
6538                     associative array)",
6539                )
6540            })?;
6541            loops.push((var, i64::from(range.left), i64::from(range.right)));
6542        }
6543        // Every index omitted: no dimension is iterated, the body never runs.
6544        if loops.is_empty() {
6545            return Ok(cur);
6546        }
6547        self.walk_foreach_nest(&loops, body_stmt, cur, None)
6548    }
6549
6550    /// A suspending `foreach` where a queue / dynamic array is nested in a
6551    /// fixed-size array or in another collection (`int a[2][3][]`, `int
6552    /// d[][]`, `int q[2][$]`). One counted loop per named dimension,
6553    /// outermost first (LRM 1800-2023 12.7.3); a fixed dimension counts its
6554    /// static range, a collection dimension counts the size of the element
6555    /// reached through the enclosing indices ([`RepeatCount::NestedSize`]),
6556    /// read when that loop is entered. An omitted index in front of a
6557    /// collection dimension and an associative array in the chain are named
6558    /// errors (the leaf lowering refuses the same shapes).
6559    fn walk_nested_foreach(
6560        &mut self,
6561        stmt: Statement<'d>,
6562        cur: BlockId,
6563    ) -> Result<BlockId, LoweringError> {
6564        let arr = stmt.expressions()[0];
6565        let body_stmt = stmt
6566            .body()
6567            .ok_or_else(|| LoweringError::malformed("ForeachLoop with no body statement"))?;
6568        let dims = stmt.foreach_loop_dims();
6569        let last_dyn = dims.iter().rposition(|d| d.range().is_none());
6570        // Whether each dimension is an associative array (its variable is a
6571        // key): peel the array type one dimension at a time.
6572        let mut ty = arr.expr_type();
6573        let mut assoc_dims = Vec::new();
6574        for _ in &dims {
6575            assoc_dims.push(ty.as_ref().is_some_and(|t| t.is_associative_array()));
6576            ty = ty.and_then(|t| t.element_type());
6577        }
6578        let mut levels = Vec::new();
6579        for (k, d) in dims.iter().enumerate() {
6580            let Some(var) = d.loop_var() else {
6581                if last_dyn.is_some_and(|l| l > k) {
6582                    return Err(LoweringError::unsupported(
6583                        "foreach whose body waits with an omitted index in front of a nested \
6584                         queue, dynamic-array or associative-array dimension",
6585                    ));
6586                }
6587                continue;
6588            };
6589            let fixed = d.range().map(|r| (r.left, r.right));
6590            levels.push(NestStep {
6591                var,
6592                fixed,
6593                assoc: assoc_dims[k],
6594            });
6595        }
6596        self.walk_nest_levels(&levels, &mut Vec::new(), arr, body_stmt, cur, None)
6597    }
6598
6599    /// The loops of [`Self::walk_nested_foreach`], `levels[0]` outermost;
6600    /// `prefix` holds the enclosing levels. `break` leaves the whole nest.
6601    fn walk_nest_levels(
6602        &mut self,
6603        levels: &[NestStep<'d>],
6604        prefix: &mut Vec<NestStep<'d>>,
6605        arr: Expression<'d>,
6606        body_stmt: Statement<'d>,
6607        cur: BlockId,
6608        outer_exit: Option<BlockId>,
6609    ) -> Result<BlockId, LoweringError> {
6610        let Some(&step_info) = levels.first() else {
6611            return self.walk_stmt(body_stmt, cur);
6612        };
6613        let var = step_info.var;
6614        let entry = self.alloc_block();
6615        self.close(cur, Terminator::Fallthrough(entry));
6616        let counter = self.alloc_repeat_counter();
6617        let (count, first, by) = match step_info.fixed {
6618            Some((left, right)) => {
6619                let by: i64 = if left <= right { 1 } else { -1 };
6620                let n = (i64::from(left) - i64::from(right)).unsigned_abs() + 1;
6621                (RepeatCount::Const(n), i64::from(left) - by, by)
6622            }
6623            None if prefix.is_empty() => (RepeatCount::Size(arr), -1, 1),
6624            None => (
6625                RepeatCount::NestedSize {
6626                    arr,
6627                    path: prefix.clone(),
6628                },
6629                -1,
6630                1,
6631            ),
6632        };
6633        self.set_repeat_init(entry, counter, count);
6634        let header = self.alloc_block();
6635        let body = self.alloc_block();
6636        let exit = self.alloc_block();
6637        self.inlined_locals.push(var);
6638        if step_info.assoc {
6639            // Keys walk first/next, like a top-level associative array.
6640            self.blocks[entry.0 as usize]
6641                .bindings
6642                .push(ArgBinding::AssocKey {
6643                    var,
6644                    arr,
6645                    path: prefix.clone(),
6646                    next: false,
6647                });
6648        } else {
6649            self.blocks[entry.0 as usize]
6650                .bindings
6651                .push(ArgBinding::Const { var, value: first });
6652            self.blocks[header.0 as usize]
6653                .bindings
6654                .push(ArgBinding::Step { var, by });
6655        }
6656        self.close(
6657            entry,
6658            Terminator::Loop {
6659                header,
6660                body,
6661                exit,
6662                entry: header,
6663            },
6664        );
6665        self.close(
6666            header,
6667            Terminator::RepeatCheck {
6668                counter,
6669                then_blk: body,
6670                else_blk: exit,
6671            },
6672        );
6673        let latch = self.alloc_block();
6674        self.set_repeat_decrement(latch, counter);
6675        if step_info.assoc {
6676            self.blocks[latch.0 as usize]
6677                .bindings
6678                .push(ArgBinding::AssocKey {
6679                    var,
6680                    arr,
6681                    path: prefix.clone(),
6682                    next: true,
6683                });
6684        }
6685        self.close(latch, Terminator::Fallthrough(header));
6686        let outer = outer_exit.unwrap_or(exit);
6687        prefix.push(step_info);
6688        let body_exit = if levels.len() == 1 {
6689            self.walk_loop_body(body_stmt, body, latch, outer)?
6690        } else {
6691            self.walk_nest_levels(&levels[1..], prefix, arr, body_stmt, body, Some(outer))?
6692        };
6693        prefix.pop();
6694        self.close(body_exit, Terminator::Fallthrough(latch));
6695        Ok(exit)
6696    }
6697
6698    /// A `foreach` over a queue, dynamic or associative array whose body
6699    /// waits: a counted loop over the size the array has at loop entry (the
6700    /// `repeat` machinery, as for a fixed-size array). The index walks up
6701    /// from 0; an associative array's key starts at its first key and moves
6702    /// to the next one after each iteration (`first`/`next`, which is how
6703    /// LRM 1800-2023 §12.7.3 orders the keys).
6704    fn walk_collection_foreach(
6705        &mut self,
6706        stmt: Statement<'d>,
6707        cur: BlockId,
6708    ) -> Result<BlockId, LoweringError> {
6709        let arr = stmt.expressions()[0];
6710        let assoc = arr.expr_type().is_some_and(|t| t.is_associative_array());
6711        let dims = stmt.foreach_loop_dims();
6712        let body_stmt = stmt
6713            .body()
6714            .ok_or_else(|| LoweringError::malformed("ForeachLoop with no body statement"))?;
6715        // The inner dimensions are fixed-size ranges (`int d[][3]`): a
6716        // collection's elements are never themselves collections, so only
6717        // the outermost dimension is dynamic (LRM 1800-2023 §12.7.3).
6718        let mut rest = Vec::new();
6719        for d in dims.iter().skip(1) {
6720            let Some(v) = d.loop_var() else {
6721                continue;
6722            };
6723            let range = d.range().ok_or_else(|| {
6724                LoweringError::unsupported(
6725                    "foreach over a dimension of a queue or dynamic array's element that is \
6726                     itself a queue, dynamic or associative array",
6727                )
6728            })?;
6729            rest.push((v, i64::from(range.left), i64::from(range.right)));
6730        }
6731        let Some(var) = dims.first().and_then(|d| d.loop_var()) else {
6732            // Outermost index omitted: only the named inner dimensions run.
6733            return if rest.is_empty() {
6734                Ok(cur)
6735            } else {
6736                self.walk_foreach_nest(&rest, body_stmt, cur, None)
6737            };
6738        };
6739        let entry = self.alloc_block();
6740        self.close(cur, Terminator::Fallthrough(entry));
6741        let counter = self.alloc_repeat_counter();
6742        self.set_repeat_init(entry, counter, RepeatCount::Size(arr));
6743        let header = self.alloc_block();
6744        let body = self.alloc_block();
6745        let exit = self.alloc_block();
6746        self.inlined_locals.push(var);
6747        if assoc {
6748            self.blocks[entry.0 as usize]
6749                .bindings
6750                .push(ArgBinding::AssocKey {
6751                    var,
6752                    arr,
6753                    path: Vec::new(),
6754                    next: false,
6755                });
6756        } else {
6757            self.blocks[entry.0 as usize]
6758                .bindings
6759                .push(ArgBinding::Const { var, value: -1 });
6760            self.blocks[header.0 as usize]
6761                .bindings
6762                .push(ArgBinding::Step { var, by: 1 });
6763        }
6764        self.close(
6765            entry,
6766            Terminator::Loop {
6767                header,
6768                body,
6769                exit,
6770                entry: header,
6771            },
6772        );
6773        self.close(
6774            header,
6775            Terminator::RepeatCheck {
6776                counter,
6777                then_blk: body,
6778                else_blk: exit,
6779            },
6780        );
6781        let latch = self.alloc_block();
6782        self.set_repeat_decrement(latch, counter);
6783        if assoc {
6784            self.blocks[latch.0 as usize]
6785                .bindings
6786                .push(ArgBinding::AssocKey {
6787                    var,
6788                    arr,
6789                    path: Vec::new(),
6790                    next: true,
6791                });
6792        }
6793        self.close(latch, Terminator::Fallthrough(header));
6794        let body_exit = if rest.is_empty() {
6795            self.walk_loop_body(body_stmt, body, latch, exit)?
6796        } else {
6797            self.walk_foreach_nest(&rest, body_stmt, body, Some(exit))?
6798        };
6799        self.close(body_exit, Terminator::Fallthrough(latch));
6800        Ok(exit)
6801    }
6802
6803    /// The counted loops of [`Self::walk_foreach_loop`], `loops[0]`
6804    /// outermost: each is the `repeat` machinery counting the dimension's
6805    /// elements, its index variable walking from the left bound to the right
6806    /// bound (it starts one step before `left`; the header advances it
6807    /// before each iteration's count check).
6808    ///
6809    /// The nest is one loop to `break`/`continue` (LRM 1800-2023 §12.7.3,
6810    /// Verilator agrees): `continue` advances to the next element (the
6811    /// innermost dimension's latch) and `break` leaves the whole nest
6812    /// (`outer_exit`, the outermost dimension's exit).
6813    fn walk_foreach_nest(
6814        &mut self,
6815        loops: &[(Symbol<'d>, i64, i64)],
6816        body_stmt: Statement<'d>,
6817        cur: BlockId,
6818        outer_exit: Option<BlockId>,
6819    ) -> Result<BlockId, LoweringError> {
6820        let Some(&(var, left, right)) = loops.first() else {
6821            return self.walk_stmt(body_stmt, cur);
6822        };
6823        let step: i64 = if left <= right { 1 } else { -1 };
6824        let count = (left - right).unsigned_abs() + 1;
6825        // A fresh block for this loop's entry: `cur` may already carry
6826        // another counted loop's `repeat_init` (an enclosing foreach body).
6827        let entry = self.alloc_block();
6828        self.close(cur, Terminator::Fallthrough(entry));
6829        let counter = self.alloc_repeat_counter();
6830        self.set_repeat_init(entry, counter, RepeatCount::Const(count));
6831        let header = self.alloc_block();
6832        let body = self.alloc_block();
6833        let exit = self.alloc_block();
6834        self.inlined_locals.push(var);
6835        self.blocks[entry.0 as usize]
6836            .bindings
6837            .push(ArgBinding::Const {
6838                var,
6839                value: left - step,
6840            });
6841        self.blocks[header.0 as usize]
6842            .bindings
6843            .push(ArgBinding::Step { var, by: step });
6844        self.close(
6845            entry,
6846            Terminator::Loop {
6847                header,
6848                body,
6849                exit,
6850                entry: header,
6851            },
6852        );
6853        self.close(
6854            header,
6855            Terminator::RepeatCheck {
6856                counter,
6857                then_blk: body,
6858                else_blk: exit,
6859            },
6860        );
6861        // The latch counts the iteration down; `continue` comes here too.
6862        let latch = self.alloc_block();
6863        self.set_repeat_decrement(latch, counter);
6864        self.close(latch, Terminator::Fallthrough(header));
6865        let outer = outer_exit.unwrap_or(exit);
6866        let body_exit = if loops.len() == 1 {
6867            self.walk_loop_body(body_stmt, body, latch, outer)?
6868        } else {
6869            self.walk_foreach_nest(&loops[1..], body_stmt, body, Some(outer))?
6870        };
6871        self.close(body_exit, Terminator::Fallthrough(latch));
6872        Ok(exit)
6873    }
6874
6875    /// R-run Task 8 (PR-045/PR-006): `repeat(N) body;` when `body` contains
6876    /// a suspend point (the gate `walk_stmt`'s own dispatch already checked
6877    /// before calling this function). Mechanically similar to
6878    /// [`Self::walk_while_loop`] -- `header`'s own terminator carries the
6879    /// real per-iteration decision, `body`'s own terminator closes the back
6880    /// edge, `entry` is `header` (the counter is checked before the first
6881    /// iteration too, LRM: `repeat(0)` runs zero times) -- except the
6882    /// decision is [`Terminator::RepeatCheck`] against a compiler-
6883    /// synthesized [`RepeatCounterId`], not a re-evaluated user `Branch`
6884    /// condition, and `cur` (this loop's own entry block) additionally
6885    /// records a [`BasicBlock::repeat_init`] evaluating the count expression
6886    /// exactly once before ever reaching `header`. The loop body is walked
6887    /// recursively via the ordinary [`Self::walk_stmt`] entry point, so any
6888    /// suspend terminator nested inside it composes for free -- exactly the
6889    /// same reason `disable` "composes correctly across suspend boundaries
6890    /// for free" per `docs/design/process.md` §2, and the same recursive
6891    /// shape every other loop-walker in this file already uses.
6892    fn walk_repeat_loop(
6893        &mut self,
6894        stmt: Statement<'d>,
6895        cur: BlockId,
6896    ) -> Result<BlockId, LoweringError> {
6897        // R-run Task 4's `stmt_shared::lower_repeat_loop` already established
6898        // (and this file's own Task 4-era doc comments confirm) that
6899        // `RepeatLoop::condition()` is the count expression, not a boolean --
6900        // slang has no separate accessor for it, this AST node just reuses
6901        // the `condition()` grammar slot for `repeat`'s own count operand.
6902        let count_expr = stmt
6903            .condition()
6904            .ok_or_else(|| LoweringError::malformed("RepeatLoop with no count expression"))?;
6905        let body_stmt = stmt
6906            .body()
6907            .ok_or_else(|| LoweringError::malformed("RepeatLoop with no body statement"))?;
6908
6909        let counter = self.alloc_repeat_counter();
6910        self.set_repeat_init(cur, counter, RepeatCount::Expr(count_expr));
6911
6912        let header = self.alloc_block();
6913        let body = self.alloc_block();
6914        let exit = self.alloc_block();
6915        // 38.36.1.2: when first encountered and at every iteration (so one
6916        // more time than the loop body runs, the last one finding the count
6917        // exhausted -- the same shape the leaf lowering has).
6918        self.probe(header, stmt);
6919        self.close(
6920            cur,
6921            Terminator::Loop {
6922                header,
6923                body,
6924                exit,
6925                entry: header,
6926            },
6927        );
6928        self.close(
6929            header,
6930            Terminator::RepeatCheck {
6931                counter,
6932                then_blk: body,
6933                else_blk: exit,
6934            },
6935        );
6936        // The latch counts the iteration down; `continue` comes here too.
6937        let latch = self.alloc_block();
6938        self.set_repeat_decrement(latch, counter);
6939        self.close(latch, Terminator::Fallthrough(header));
6940        let body_exit = self.walk_loop_body(body_stmt, body, latch, exit)?;
6941        self.close(body_exit, Terminator::Fallthrough(latch));
6942        Ok(exit)
6943    }
6944}
6945
6946/// Whether `node` (a statement kept as one straight-line leaf) contains a
6947/// jump out of itself into the CFG around it: a `break`/`continue` not
6948/// inside a loop of its own (when `in_loop`, an enclosing CFG loop exists)
6949/// or a `return` (when `in_task`, the statement is part of an inlined task
6950/// body).
6951fn escapes(node: SemNode<'_>, in_loop: bool, in_task: bool) -> bool {
6952    let mut stack = vec![(node, false)];
6953    while let Some((n, nested_loop)) = stack.pop() {
6954        let mut inner = nested_loop;
6955        if let Some(s) = n.as_statement() {
6956            let kind = s.kind();
6957            if matches!(kind, StatementKind::Break | StatementKind::Continue)
6958                && in_loop
6959                && !nested_loop
6960            {
6961                return true;
6962            }
6963            if kind == StatementKind::Return && in_task {
6964                return true;
6965            }
6966            if matches!(
6967                kind,
6968                StatementKind::ForLoop
6969                    | StatementKind::WhileLoop
6970                    | StatementKind::DoWhileLoop
6971                    | StatementKind::ForeverLoop
6972                    | StatementKind::ForeachLoop
6973                    | StatementKind::RepeatLoop
6974            ) {
6975                inner = true;
6976            }
6977        }
6978        stack.extend(n.children().into_iter().map(|c| (c, inner)));
6979    }
6980    false
6981}
6982
6983/// Whether `stmt` is a statement a VPI `cbStmt` callback fires for once, just
6984/// before it executes (IEEE 1800-2023 Table 38-6): the blocks, `if`, `case`,
6985/// `wait`, the procedural assignments / `force` / `release` / `disable` /
6986/// event triggers, task and system-task calls, and the delay and event
6987/// controls. This mirrors the statements the VPI model gives an object of
6988/// those types. The loops (`while`/`for`/`repeat`/`forever`) fire per
6989/// iteration instead and are not covered here.
6990pub fn is_entry_probe_kind(stmt: &Statement<'_>) -> bool {
6991    match stmt.kind() {
6992        StatementKind::Block
6993        | StatementKind::Conditional
6994        | StatementKind::Case
6995        | StatementKind::PatternCase
6996        | StatementKind::Wait
6997        | StatementKind::Disable
6998        | StatementKind::EventTrigger
6999        | StatementKind::ProceduralAssign
7000        | StatementKind::ProceduralDeassign => true,
7001        StatementKind::ExpressionStatement => stmt.expr().is_some_and(|e| {
7002            matches!(
7003                e.kind(),
7004                ExpressionKind::Assignment | ExpressionKind::Call | ExpressionKind::UnaryOp
7005            )
7006        }),
7007        StatementKind::Timed => {
7008            stmt.timing_control().map(|t| t.kind()) != Some(TimingControlKind::RepeatedEvent)
7009        }
7010        // Not in Table 38-6, or a loop (probed per iteration): no entry probe.
7011        StatementKind::Invalid
7012        | StatementKind::Empty
7013        | StatementKind::List
7014        | StatementKind::VariableDeclaration
7015        | StatementKind::Return
7016        | StatementKind::Continue
7017        | StatementKind::Break
7018        | StatementKind::DisableFork
7019        | StatementKind::ForLoop
7020        | StatementKind::RepeatLoop
7021        | StatementKind::ForeachLoop
7022        | StatementKind::WhileLoop
7023        | StatementKind::DoWhileLoop
7024        | StatementKind::ForeverLoop
7025        | StatementKind::ImmediateAssertion
7026        | StatementKind::ConcurrentAssertion
7027        | StatementKind::WaitFork
7028        | StatementKind::WaitOrder
7029        | StatementKind::RandCase
7030        | StatementKind::RandSequence
7031        | StatementKind::ProceduralChecker => false,
7032        other => unreachable!("statement kind {other:?} is newer than this classification"),
7033    }
7034}
7035
7036thread_local! {
7037    /// The text of each source file of the design (as it was parsed, so
7038    /// offsets of a [`sv_lang::SourceLoc`] index it), which `disable_label`
7039    /// reads a `disable` statement's target from, by file name. One file
7040    /// registered without a name ([`set_source_text`]) answers for every
7041    /// location.
7042    static DISABLE_SOURCE: std::cell::RefCell<Vec<(Option<String>, std::rc::Rc<str>)>> =
7043        const { std::cell::RefCell::new(Vec::new()) };
7044}
7045
7046/// Registers the text of the source file statements are elaborated from,
7047/// for `disable label;` (see [`disable_label`]); `None` clears it.
7048pub fn set_source_text(text: Option<&str>) {
7049    DISABLE_SOURCE.with(|s| {
7050        *s.borrow_mut() = text
7051            .map(|t| (None, std::rc::Rc::from(t)))
7052            .into_iter()
7053            .collect();
7054    });
7055    scan_disable_names(text.into_iter());
7056}
7057
7058thread_local! {
7059    /// What the registered source text names (see [`scan_disable_names`]):
7060    /// every `disable <name>;` target, and every task name.
7061    static DISABLE_NAMES: std::cell::RefCell<(
7062        std::collections::HashSet<String>,
7063        std::collections::HashSet<String>,
7064    )> = std::cell::RefCell::new(Default::default());
7065}
7066
7067/// Records which names a `disable <name>;` of the design targets, and which
7068/// names are tasks, from the registered source text (the binding cannot read a
7069/// `disable` target, see the module doc). Over-approximate on purpose (a word
7070/// in a comment counts): a name here only makes the builder keep a scope exit
7071/// for it ([`CfgBuilder::scope_chain`]) or accept it as a task of the design.
7072fn scan_disable_names<'a>(texts: impl Iterator<Item = &'a str>) {
7073    let mut labels = std::collections::HashSet::new();
7074    let mut tasks = std::collections::HashSet::new();
7075    let mut fn_labels = std::collections::HashSet::new();
7076    let ident = |c: u8| c == b'_' || c == b'$' || c.is_ascii_alphanumeric();
7077    for text in texts {
7078        let b = text.as_bytes();
7079        let word_at = |mut i: usize| -> Option<(&str, usize)> {
7080            while i < b.len() && b[i].is_ascii_whitespace() {
7081                i += 1;
7082            }
7083            let start = i;
7084            while i < b.len() && ident(b[i]) {
7085                i += 1;
7086            }
7087            (i > start).then(|| (&text[start..i], i))
7088        };
7089        let mut i = 0;
7090        // Inside a `function` body (a DPI prototype or `pure`/`extern`
7091        // declaration has none, and ends at its `;`).
7092        let (mut in_fn, mut proto) = (false, false);
7093        while i < b.len() {
7094            if !ident(b[i]) {
7095                if b[i] == b';' {
7096                    proto = false;
7097                }
7098                i += 1;
7099                continue;
7100            }
7101            let start = i;
7102            while i < b.len() && ident(b[i]) {
7103                i += 1;
7104            }
7105            let boundary = start == 0 || (b[start - 1] != b'.' && b[start - 1] != b'`');
7106            match &text[start..i] {
7107                "import" | "export" | "extern" | "pure" if boundary => proto = true,
7108                "function" if boundary => in_fn = !proto,
7109                "endfunction" if boundary => in_fn = false,
7110                "disable" if boundary => {
7111                    // `disable a.b.c;` (9.6.2) names its last segment.
7112                    // A generate-block index (`g[0].bk`) is part of a segment.
7113                    let skip_idx = |mut e: usize| -> usize {
7114                        loop {
7115                            let t = text[e..].trim_start();
7116                            let lead = text[e..].len() - t.len();
7117                            let Some(inner) = t.strip_prefix('[') else {
7118                                return e;
7119                            };
7120                            match inner.find(']') {
7121                                Some(c)
7122                                    if inner[..c].trim().bytes().all(|d| d.is_ascii_digit()) =>
7123                                {
7124                                    e += lead + 1 + c + 1;
7125                                }
7126                                _ => return e,
7127                            }
7128                        }
7129                    };
7130                    if let Some((mut name, e0)) = word_at(i)
7131                        && name != "fork"
7132                    {
7133                        let mut end = skip_idx(e0);
7134                        while text[end..].trim_start().starts_with('.')
7135                            && let Some((next, e)) = word_at(
7136                                end + (text[end..].len() - text[end..].trim_start().len()) + 1,
7137                            )
7138                        {
7139                            name = next;
7140                            end = skip_idx(e);
7141                        }
7142                        if text[end..].trim_start().starts_with(';') {
7143                            labels.insert(name.to_string());
7144                            if in_fn {
7145                                fn_labels.insert(name.to_string());
7146                            }
7147                            // The whole dotted path too ([`CfgBuilder::name_path`]).
7148                            let full: String = text[i..end]
7149                                .chars()
7150                                .filter(|c| !c.is_ascii_whitespace())
7151                                .collect();
7152                            // Every dotted suffix too: `disable u1.main.inner;`
7153                            // names the block `inner` of `main` by the path
7154                            // below the instance (9.6.2).
7155                            let mut rest = full.as_str();
7156                            while let Some((_, tail)) = rest.split_once('.') {
7157                                if tail.contains('.') {
7158                                    labels.insert(tail.to_string());
7159                                }
7160                                rest = tail;
7161                            }
7162                            if full.contains('.') {
7163                                labels.insert(full);
7164                            }
7165                        }
7166                    }
7167                }
7168                "task" if boundary => {
7169                    if let Some((mut name, end)) = word_at(i) {
7170                        if matches!(name, "automatic" | "static")
7171                            && let Some((n, _)) = word_at(end)
7172                        {
7173                            name = n;
7174                        }
7175                        tasks.insert(name.to_string());
7176                    }
7177                }
7178                _ => {}
7179            }
7180        }
7181    }
7182    DISABLE_NAMES.with(|n| *n.borrow_mut() = (labels, tasks));
7183    FN_DISABLE_LABELS.with(|n| *n.borrow_mut() = fn_labels);
7184}
7185
7186thread_local! {
7187    /// The targets of the `disable <name>;` statements written inside a
7188    /// `function` body (see [`scan_disable_names`]).
7189    static FN_DISABLE_LABELS: std::cell::RefCell<std::collections::HashSet<String>> =
7190        std::cell::RefCell::new(Default::default());
7191}
7192
7193/// Whether the design has a `disable <name>;` inside a function body: such a
7194/// function can end a block of its caller (LRM 9.6.2, 35.9), which the call
7195/// sites check for ([`SwitchOn::FnDisable`]). Designs without one pay nothing.
7196pub fn has_function_disables() -> bool {
7197    FN_DISABLE_LABELS.with(|n| !n.borrow().is_empty())
7198}
7199
7200/// Whether a function body of the design has `disable <label>;`.
7201pub fn is_function_disable_label(label: &str) -> bool {
7202    FN_DISABLE_LABELS.with(|n| n.borrow().contains(label))
7203}
7204
7205/// Whether some `disable` statement of the design names `label`.
7206fn is_disabled_somewhere(label: &str) -> bool {
7207    DISABLE_NAMES.with(|n| n.borrow().0.contains(label))
7208}
7209
7210/// Every dotted `disable a.b.c;` target (and dotted suffix of one) the design
7211/// writes, sorted.
7212pub fn dotted_disable_labels() -> Vec<String> {
7213    let mut v: Vec<String> = DISABLE_NAMES.with(|n| {
7214        n.borrow()
7215            .0
7216            .iter()
7217            .filter(|l| l.contains('.'))
7218            .cloned()
7219            .collect()
7220    });
7221    v.sort();
7222    v
7223}
7224
7225/// Whether `label` is the name of a task of the design.
7226pub fn is_task_name(label: &str) -> bool {
7227    DISABLE_NAMES.with(|n| n.borrow().1.contains(label))
7228}
7229
7230/// Registers the text of every source file of a multi-file design, each
7231/// under the file name its locations resolve to ([`set_source_session`]).
7232pub fn set_source_texts(files: Vec<(String, String)>) {
7233    scan_disable_names(files.iter().map(|(_, t)| t.as_str()));
7234    DISABLE_SOURCE.with(|s| {
7235        *s.borrow_mut() = files
7236            .into_iter()
7237            .map(|(name, text)| (Some(name), std::rc::Rc::from(text)))
7238            .collect();
7239    });
7240}
7241
7242/// Whether two file names denote the same file (equal, or the same file on
7243/// disk).
7244fn same_file(a: &str, b: &str) -> bool {
7245    a == b
7246        || std::fs::canonicalize(a)
7247            .ok()
7248            .is_some_and(|x| std::fs::canonicalize(b).ok() == Some(x))
7249}
7250
7251/// The registered text holding `at`: the unnamed one if that is what was
7252/// registered, else the named file `at` resolves to (through the session
7253/// registered by [`set_source_session`]).
7254fn source_text_at(at: sv_lang::SourceLoc) -> Option<std::rc::Rc<str>> {
7255    let files = DISABLE_SOURCE.with(|s| s.borrow().clone());
7256    if let [(None, text)] = files.as_slice() {
7257        return Some(text.clone());
7258    }
7259    let file = SOURCE_SESSION
7260        .with(|s| s.borrow().as_ref().and_then(|se| se.resolve_location(at)))?
7261        .file;
7262    files
7263        .into_iter()
7264        .find(|(n, _)| n.as_deref().is_some_and(|n| same_file(n, &file)))
7265        .map(|(_, text)| text)
7266}
7267
7268thread_local! {
7269    /// The main source file's name as given (`$info`'s `file:line`).
7270    static SOURCE_NAME: std::cell::RefCell<Option<String>> =
7271        const { std::cell::RefCell::new(None) };
7272}
7273
7274/// Registers the main source file's name (see [`source_location`]).
7275pub fn set_source_name(name: Option<&str>) {
7276    SOURCE_NAME.with(|s| *s.borrow_mut() = name.map(str::to_owned));
7277}
7278
7279/// The source text of `span`, from the file text registered by
7280/// [`set_source_text`] / [`set_source_texts`] (`None` if that file's text is
7281/// not registered, e.g. a span in an `` `include `` d file).
7282pub fn source_text_range(span: sv_lang::SourceSpan) -> Option<String> {
7283    source_text_at(span.start)?
7284        .get(span.start.offset as usize..span.end.offset as usize)
7285        .map(str::to_owned)
7286}
7287
7288thread_local! {
7289    /// The session the design was parsed with: resolves a source location
7290    /// to its file and line (included files and `` `line `` directives
7291    /// included).
7292    static SOURCE_SESSION: std::cell::RefCell<Option<sv_lang::Session>> =
7293        const { std::cell::RefCell::new(None) };
7294}
7295
7296/// Registers the session the design was parsed with (see
7297/// [`source_location`]); `None` clears it.
7298pub fn set_source_session(session: Option<sv_lang::Session>) {
7299    SOURCE_SESSION.with(|s| *s.borrow_mut() = session);
7300}
7301
7302/// `file:line` of `loc`, as slang's source manager reports it (honoring
7303/// `` `include `` and `` `line ``) through the session registered by
7304/// [`set_source_session`] -- the main source file under the name it was
7305/// given ([`set_source_name`]); without a session, counted in the main
7306/// source file registered by [`set_source_name`]/[`set_source_text`].
7307pub fn source_location(loc: sv_lang::SourceLoc) -> Option<String> {
7308    if let Some(at) = SOURCE_SESSION.with(|s| {
7309        s.borrow()
7310            .as_ref()
7311            .and_then(|session| session.resolve_location(loc))
7312    }) {
7313        let main = SOURCE_NAME.with(|s| s.borrow().clone());
7314        let file = match main {
7315            Some(m) if same_file(&at.file, &m) => m,
7316            _ => at.file,
7317        };
7318        return Some(format!("{file}:{}", at.line));
7319    }
7320    let name = SOURCE_NAME.with(|s| s.borrow().clone())?;
7321    let text = source_text_at(loc)?;
7322    let head = text.get(..(loc.offset as usize).min(text.len()))?;
7323    let line = head.bytes().filter(|&b| b == b'\n').count() + 1;
7324    Some(format!("{name}:{line}"))
7325}
7326
7327/// The segments of a hierarchical `disable a.b.c;` target (LRM 9.6.2), read
7328/// from the statement's source text like [`disable_label`]; `None` for a
7329/// simple or unreadable target.
7330pub fn disable_path(stmt: &Statement<'_>) -> Option<Vec<String>> {
7331    let text = source_text_range(stmt.source_range())?;
7332    let rest = text.trim().strip_prefix("disable")?;
7333    if !rest.starts_with(char::is_whitespace) {
7334        return None;
7335    }
7336    let label: String = rest
7337        .trim()
7338        .trim_end_matches(';')
7339        .chars()
7340        .filter(|c| !c.is_whitespace())
7341        .collect();
7342    let segs: Vec<String> = label.split('.').map(str::to_string).collect();
7343    // A generate-block segment carries its index (`g[0]`).
7344    let ident = |t: &str| {
7345        let t = match t.find('[') {
7346            Some(b)
7347                if t.ends_with(']')
7348                    && t[b + 1..t.len() - 1].bytes().all(|d| d.is_ascii_digit())
7349                    && b + 2 < t.len() =>
7350            {
7351                &t[..b]
7352            }
7353            Some(_) => return false,
7354            None => t,
7355        };
7356        !t.is_empty()
7357            && t.chars().enumerate().all(|(i, c)| {
7358                c == '_' || c.is_ascii_alphabetic() || (i > 0 && (c.is_ascii_digit() || c == '$'))
7359            })
7360    };
7361    (segs.len() > 1 && segs.iter().all(|t| ident(t))).then_some(segs)
7362}
7363
7364/// The name `disable` acts on in a statement the CFG builder kept: the plain
7365/// label, or the last segment of a hierarchical `a.b.c` the builder resolved to
7366/// a block of another process ([`disable_path`]).
7367pub fn disable_leaf_label(stmt: &Statement<'_>) -> Result<String, LoweringError> {
7368    match disable_path(stmt).and_then(|p| p.last().cloned()) {
7369        Some(leaf) => Ok(leaf),
7370        None => disable_label(stmt),
7371    }
7372}
7373
7374/// The target name of a `disable label;` statement. The pinned sv-lang
7375/// binding exposes no `DisableStatement::target` (see the module doc), so
7376/// it is read from the statement's own source span in the text registered
7377/// by [`set_source_text`] -- accepted only when that span reads exactly
7378/// `disable <identifier> ;`, so a span into another file (an `` `include ``)
7379/// is a named error, never a misread label.
7380pub fn disable_label(stmt: &Statement<'_>) -> Result<String, LoweringError> {
7381    let span = stmt.source_range();
7382    let text = source_text_range(span);
7383    let unreadable = || {
7384        LoweringError::unsupported(
7385            "disable statement whose target cannot be read (no registered source text, or \
7386             the statement is not in a registered source file)",
7387        )
7388    };
7389    let text = text.ok_or_else(unreadable)?;
7390    let rest = text.trim().strip_prefix("disable").ok_or_else(unreadable)?;
7391    let label = rest.trim().trim_end_matches(';').trim();
7392    let ident = !label.is_empty()
7393        && label.chars().enumerate().all(|(i, c)| {
7394            c == '_' || c.is_ascii_alphabetic() || (i > 0 && (c.is_ascii_digit() || c == '$'))
7395        });
7396    if !ident || !rest.starts_with(char::is_whitespace) {
7397        let hier = if label.contains('.') {
7398            "; disabling a task or interface method by hierarchical name would have to \
7399             end every activation of it in other processes (IEEE 1800-2023 9.6.2)"
7400        } else {
7401            ""
7402        };
7403        return Err(LoweringError::unsupported(format!(
7404            "disable of `{label}` (only a simple block name is supported{hier})"
7405        )));
7406    }
7407    Ok(label.to_string())
7408}
7409
7410/// Whether `node`'s subtree contains a `disable label;` statement, in itself
7411/// or in the body of a task it calls (an inlined task's `disable` may target
7412/// the caller's block, LRM 9.6.2).
7413fn contains_disable_through_calls(node: SemNode<'_>) -> bool {
7414    let mut visiting = std::collections::HashSet::new();
7415    let mut stack = vec![node];
7416    while let Some(n) = stack.pop() {
7417        if n.as_statement()
7418            .is_some_and(|s| s.kind() == StatementKind::Disable)
7419        {
7420            return true;
7421        }
7422        if let Some(e) = n.as_expression()
7423            && let Some(callee) = user_task_callee(&e)
7424            && let Some(body) = proc_body(&callee)
7425            && visiting.insert(callee.uid())
7426        {
7427            stack.push(body.as_sem_node());
7428        }
7429        stack.extend(live_children(&n));
7430    }
7431    false
7432}
7433
7434/// Whether `node`'s subtree holds a `fork`/`join*` block, in itself or in the
7435/// body of a task it calls.
7436fn contains_fork_through_calls(node: SemNode<'_>) -> bool {
7437    let mut visiting = std::collections::HashSet::new();
7438    let mut stack = vec![node];
7439    while let Some(n) = stack.pop() {
7440        if n.as_statement().is_some_and(|s| {
7441            matches!(
7442                s.block_kind(),
7443                Some(
7444                    StatementBlockKind::JoinAll
7445                        | StatementBlockKind::JoinAny
7446                        | StatementBlockKind::JoinNone
7447                )
7448            )
7449        }) {
7450            return true;
7451        }
7452        if let Some(e) = n.as_expression()
7453            && let Some(callee) = user_task_callee(&e)
7454            && let Some(body) = proc_body(&callee)
7455            && visiting.insert(callee.uid())
7456        {
7457            stack.push(body.as_sem_node());
7458        }
7459        stack.extend(live_children(&n));
7460    }
7461    false
7462}
7463
7464/// Every `StatementKind` variant [`CfgBuilder::walk_stmt`] treats as ordinary
7465/// straight-line code (PR-006's own acceptance text) -- i.e. every variant
7466/// that is not one of the 5 explicitly-named statement kinds
7467/// (`List`/`Block`/`Conditional`/`Timed`/the 5 suspend-rejecting kinds) it
7468/// handles directly. Kept as a `matches!` (not a literal `match`, see
7469/// `walk_stmt`'s doc comment) enumerating all 22 remaining variants of
7470/// today's pinned `StatementKind::COUNT == 32`.
7471/// Task 1 removed `ForLoop`/`WhileLoop`/`DoWhileLoop`/`ForeverLoop`/
7472/// `ForeachLoop`/`Disable` from this set. The five loop kinds now dispatch
7473/// to dedicated handlers (PR-045's `Terminator::Loop`) instead of being
7474/// treated as opaque leaves; `Disable` also gets a dedicated `walk_stmt` arm
7475/// (a jump to its named block's exit -- see this file's module doc comment's
7476/// `disable label;` section), never the "push as ordinary code" treatment
7477/// this function's callers give everything below. `RepeatLoop` (a plain statement-level `repeat(N)
7478/// stmt;`, distinct from the `RepeatedEvent` *timing control*) stays
7479/// ordinary -- PR-045's own requirement text names exactly `for`/`while`/
7480/// `do-while`/`forever`/`foreach`, not `repeat`.
7481fn is_ordinary_statement_kind(kind: StatementKind) -> bool {
7482    matches!(
7483        kind,
7484        StatementKind::Empty
7485            | StatementKind::ExpressionStatement
7486            | StatementKind::VariableDeclaration
7487            | StatementKind::Return
7488            | StatementKind::Continue
7489            | StatementKind::Break
7490            | StatementKind::Case
7491            | StatementKind::PatternCase
7492            | StatementKind::RepeatLoop
7493            | StatementKind::ImmediateAssertion
7494            | StatementKind::ConcurrentAssertion
7495            | StatementKind::ProceduralAssign
7496            | StatementKind::ProceduralDeassign
7497            | StatementKind::RandCase
7498            | StatementKind::RandSequence
7499            | StatementKind::ProceduralChecker
7500    )
7501}
7502
7503/// R-run Task 1: `true` iff `stmt` (already known to be a `StatementKind::
7504/// ExpressionStatement`) is a bare `$finish`/`$finish(n)`/`$stop`/`$fatal`
7505/// system-call TASK statement -- the exact same `ExpressionKind::Call` +
7506/// `is_system_call() == Some(true)` + `system_subroutine().known_name_id()`
7507/// recognition `sv-sim-codegen::process_lower::lower_system_call_statement`
7508/// already uses for its own dispatch (mirrored, not re-derived differently:
7509/// both call sites must agree on what counts as one of these three calls).
7510/// `$fatal`'s own optional finish_number/message arguments and `$stop`'s own
7511/// interactive-debugger-break runtime semantics are unaffected by this CFG-
7512/// level check -- it only ever answers "does the calling process's own
7513/// control flow end here", never anything about the call's own argument
7514/// list or its eventual codegen lowering (`$stop`/`$fatal` still lower to a
7515/// named `LoweringError` in `process_lower.rs` today, exactly as before this
7516/// task; this predicate firing for them one file earlier changes nothing
7517/// observable about that until a later task actually lowers them).
7518fn is_finish_or_stop_call(stmt: &Statement<'_>) -> bool {
7519    let Some(e) = stmt.expr() else {
7520        return false;
7521    };
7522    if e.kind() != ExpressionKind::Call || e.is_system_call() != Some(true) {
7523        return false;
7524    }
7525    let Some(sub) = e.system_subroutine() else {
7526        return false;
7527    };
7528    matches!(
7529        sub.known_name_id(),
7530        KnownSystemName::Finish | KnownSystemName::Stop | KnownSystemName::Fatal
7531    )
7532}
7533
7534/// The `unique`/`unique0`/`priority` qualifier on an `if` or `case` statement
7535/// (LRM 1800-2023 §12.4.2, §12.5.3), `None` for an unqualified statement or
7536/// any other kind. A `case ... matches` ([`StatementKind::PatternCase`])
7537/// carries its qualifier under its own accessor.
7538pub fn violation_check(stmt: &Statement<'_>) -> Option<UniquePriorityCheck> {
7539    let check = if stmt.kind() == StatementKind::PatternCase {
7540        stmt.pattern_case_check()
7541    } else {
7542        stmt.check()
7543    };
7544    match check {
7545        Some(UniquePriorityCheck::None) | None => None,
7546        Some(check) => Some(check),
7547    }
7548}
7549
7550/// The `if` / `else if` series a `unique`/`unique0`/`priority` qualifier on
7551/// the first `if` applies to (§12.4.2: "the entire series of if-else-if
7552/// conditions"): the condition and then-statement of each link, in source
7553/// order, and the final `else` statement if there is one. A nested
7554/// `else if` carries no qualifier of its own (the grammar has none); an
7555/// `else` holding a qualified `if` of its own is the final `else` and a
7556/// separate statement.
7557pub struct IfChain<'d> {
7558    /// The `if` statement of each link (its predicate is `links[i]
7559    /// .conditions()`: plain, `&&&`-chained, or pattern-matching, §12.6).
7560    pub links: Vec<Statement<'d>>,
7561    pub thens: Vec<Statement<'d>>,
7562    pub otherwise: Option<Statement<'d>>,
7563}
7564
7565/// Peels `Invalid` wrappers off `stmt` (IEEE 1800-2023 12.4/12.5).
7566///
7567/// slang suppresses the error of a construct inside a statically dead `if`
7568/// or `case` arm (e.g. a reversed range select `v[7:8]` of a descending
7569/// vector under a false parameter) but demotes every enclosing statement to
7570/// `Invalid`. Such wrappers nest (an `Invalid` body wraps a `Block` that wraps
7571/// an `Invalid` wrapping the `Conditional`), so this unwraps recursively via
7572/// `Statement::invalid_child`. An `Invalid` with no recoverable child is
7573/// returned unchanged, so the caller's loud "Invalid statement" refusal still
7574/// fires for it.
7575pub fn unwrap_invalid<'d>(mut stmt: Statement<'d>) -> Statement<'d> {
7576    while stmt.kind() == StatementKind::Invalid {
7577        match stmt.invalid_child() {
7578            Some(child) => stmt = child,
7579            None => break,
7580        }
7581    }
7582    stmt
7583}
7584
7585/// The child statements of a `List`/`Block`, with each statement slang
7586/// demoted to `Invalid` (a statically dead `if`/`case` arm it could not
7587/// bind) unwrapped to the real one. `Statement::statements` skips `Invalid`
7588/// children, which would silently drop the live statement (LRM 12.4/12.5).
7589pub fn live_statements<'d>(stmt: &Statement<'d>) -> Vec<Statement<'d>> {
7590    stmt.as_sem_node()
7591        .children_with_invalid()
7592        .into_iter()
7593        .filter_map(|n| n.as_statement())
7594        .map(unwrap_invalid)
7595        .collect()
7596}
7597
7598/// The children of `node` for a whole-subtree walk, with each `Invalid`
7599/// statement or expression (slang's demotion of a construct inside a
7600/// statically dead `if`/`case` arm it could not bind, LRM 12.4/12.5) replaced
7601/// by the real node it wraps. `SemNode::children` skips `Invalid` children,
7602/// so a call or read inside the live arm of such a statement would otherwise
7603/// be invisible to every discovery walk (called-subroutine discovery, write
7604/// sets, unsupported-construct scans).
7605pub fn live_children<'d>(node: &SemNode<'d>) -> Vec<SemNode<'d>> {
7606    node.children_with_invalid()
7607        .into_iter()
7608        .map(|n| {
7609            if let Some(s) = n.as_statement() {
7610                return unwrap_invalid(s).as_sem_node();
7611            }
7612            if let Some(mut e) = n.as_expression() {
7613                while e.kind() == ExpressionKind::Invalid
7614                    && let Some(c) = e.invalid_child()
7615                {
7616                    e = c;
7617                }
7618                return e.as_sem_node();
7619            }
7620            n
7621        })
7622        .collect()
7623}
7624
7625/// The body statement of a procedural block with any `Invalid` wrappers from
7626/// a statically dead arm peeled off (see [`unwrap_invalid`]). Every pass that
7627/// inspects a process's head timing control, read set or statements must go
7628/// through this rather than `Symbol::body`, so they all see the same live
7629/// body (otherwise an `always @*` loses its sensitivity).
7630pub fn proc_body<'d>(block: &Symbol<'d>) -> Option<Statement<'d>> {
7631    block.body().map(unwrap_invalid)
7632}
7633
7634/// The `StatementBlock` scopes whose locals `stmt` (a `Block`) declares.
7635///
7636/// Normally that is `Statement::block_symbol()`. slang never attaches the
7637/// symbol to a block that failed to bind (`Statement::bindBlock` returns the
7638/// `Invalid` wrapper before setting `blockSymbol`), so a `begin`/`end` that
7639/// holds a statically dead `if`/`case` arm it could not bind (LRM 12.4/12.5)
7640/// is recovered through `invalid_child` with no symbol, and its locals would
7641/// be invisible. For such a block the scopes are recovered from the variables
7642/// the live subtree references: the `StatementBlock` parents of its
7643/// `NamedValue` symbols. That can include an enclosing or nested block's
7644/// scope, which is harmless for the union-style local collections that call
7645/// this; a local no live statement references is not recovered (it has no
7646/// observable use).
7647pub fn block_scopes<'d>(stmt: &Statement<'d>) -> Vec<Symbol<'d>> {
7648    if let Some(bs) = stmt.block_symbol() {
7649        return vec![bs];
7650    }
7651    let node = stmt.as_sem_node();
7652    if stmt.kind() != StatementKind::Block || !contains_invalid(node) {
7653        return Vec::new();
7654    }
7655    let mut out: Vec<Symbol<'d>> = Vec::new();
7656    let mut seen = HashSet::new();
7657    let mut stack = vec![node];
7658    while let Some(n) = stack.pop() {
7659        if let Some(e) = n.as_expression()
7660            && crate::symid::is_value_name(e.kind())
7661            && let Some(sym) = e.referenced_symbol()
7662            && let Some(parent) = sym.parent()
7663            && parent.kind() == SymbolKind::StatementBlock
7664            && seen.insert(parent.uid())
7665        {
7666            out.push(parent);
7667        }
7668        stack.extend(live_children(&n));
7669    }
7670    out
7671}
7672
7673/// The name of a `begin`/`end` or `fork`/`join` block statement (LRM 9.3.4),
7674/// empty when unnamed.
7675///
7676/// Normally `Statement::block_symbol()`'s name. A block that failed to bind
7677/// (a statically dead arm slang could not bind, LRM 12.4) carries no symbol,
7678/// so its name would be lost and `disable blk` (LRM 9.6.2) refused. For such
7679/// a block the name is read back from the statement's own source span in the
7680/// text registered by [`set_source_text`]: either the `begin : name` form or
7681/// a `name : begin` statement label. Anything else reads as unnamed.
7682pub fn block_name(stmt: &Statement<'_>) -> String {
7683    if let Some(bs) = stmt.block_symbol() {
7684        return bs.name().to_string();
7685    }
7686    if stmt.kind() != StatementKind::Block || !contains_invalid(stmt.as_sem_node()) {
7687        return String::new();
7688    }
7689    let Some(text) = source_text_range(stmt.source_range()) else {
7690        return String::new();
7691    };
7692    let ident = |s: &str| -> Option<(String, String)> {
7693        let s = s.trim_start();
7694        let n = s
7695            .char_indices()
7696            .take_while(|&(i, c)| {
7697                c == '_' || c.is_ascii_alphabetic() || (i > 0 && (c.is_ascii_digit() || c == '$'))
7698            })
7699            .count();
7700        (n > 0).then(|| (s[..n].to_string(), s[n..].to_string()))
7701    };
7702    let Some((first, rest)) = ident(&text) else {
7703        return String::new();
7704    };
7705    if matches!(first.as_str(), "begin" | "fork") {
7706        // `begin : name`; a bare `begin` followed by a statement is unnamed.
7707        return match rest.trim_start().strip_prefix(':').and_then(ident) {
7708            Some((name, _)) => name,
7709            None => String::new(),
7710        };
7711    }
7712    // `name : begin`.
7713    match rest.trim_start().strip_prefix(':').and_then(ident) {
7714        Some((kw, _)) if matches!(kw.as_str(), "begin" | "fork") => first,
7715        _ => String::new(),
7716    }
7717}
7718
7719/// Whether any node under `node` is an `Invalid` statement or expression.
7720fn contains_invalid(node: SemNode<'_>) -> bool {
7721    let is_invalid = node
7722        .as_statement()
7723        .is_some_and(|s| s.kind() == StatementKind::Invalid)
7724        || node
7725            .as_expression()
7726            .is_some_and(|e| e.kind() == ExpressionKind::Invalid);
7727    is_invalid
7728        || node
7729            .children_with_invalid()
7730            .into_iter()
7731            .any(contains_invalid)
7732}
7733
7734/// For a plain `case` whose selector and labels are all compile-time
7735/// constants and whose arms include an `Invalid` one (a statically dead arm
7736/// slang could not bind, LRM 12.5): `Some(arm)` names the one statement that
7737/// can ever run (`None` inside: no arm matches and there is no `default`).
7738/// `None` for any case this does not apply to (non-constant, `casez`/`casex`/
7739/// `inside`, a call in a label, or no `Invalid` arm), which lowers as usual.
7740pub fn constant_case_live_arm<'d>(stmt: &Statement<'d>) -> Option<Option<Statement<'d>>> {
7741    fn has_call(e: Expression<'_>) -> bool {
7742        e.kind() == ExpressionKind::Call || e.operands().into_iter().any(has_call)
7743    }
7744    if stmt.kind() != StatementKind::Case
7745        || stmt.case_condition() != Some(sv_lang::CaseStatementCondition::Normal)
7746        || violation_check(stmt).is_some()
7747    {
7748        return None;
7749    }
7750    let items = stmt.case_items();
7751    let default = stmt.default_case();
7752    if !items
7753        .iter()
7754        .map(|i| i.stmt())
7755        .chain(default)
7756        .any(|a| contains_invalid(a.as_sem_node()))
7757    {
7758        return None;
7759    }
7760    let sel = stmt.expr()?;
7761    if has_call(sel) {
7762        return None;
7763    }
7764    let sv = sel.constant_value()?.as_i64()?;
7765    for item in &items {
7766        for label in item.expressions() {
7767            if has_call(label) {
7768                return None;
7769            }
7770            if label.constant_value()?.as_i64()? == sv {
7771                return Some(Some(item.stmt()));
7772            }
7773        }
7774    }
7775    Some(default)
7776}
7777
7778/// The fixed truth of a plain `if`'s predicate when it is a compile-time
7779/// constant with no function call in it (`if (PARAM > 0)`): `Some(taken)`
7780/// names the one arm that can ever run, so the other need not be lowered
7781/// (LRM 1800-2023 12.4; the dead arm may be legal yet unlowerable once
7782/// its constants fold, e.g. an out-of-range part-select, 11.5.1). A call is
7783/// excluded because the evaluator would run it at compile time, skipping
7784/// its side effects. `None` for any other `if`.
7785pub fn constant_if_arm(stmt: &Statement<'_>) -> Option<bool> {
7786    fn has_call(e: Expression<'_>) -> bool {
7787        e.kind() == ExpressionKind::Call || e.operands().into_iter().any(has_call)
7788    }
7789    let conditions = stmt.conditions();
7790    if stmt.kind() != StatementKind::Conditional
7791        || conditions.len() != 1
7792        || conditions[0].pattern().is_some()
7793        || violation_check(stmt).is_some()
7794    {
7795        return None;
7796    }
7797    let cond = conditions[0].expr();
7798    if has_call(cond) {
7799        return None;
7800    }
7801    cond.constant_value()
7802        .and_then(|c| c.as_i64())
7803        .map(|c| c != 0)
7804}
7805
7806/// Whether `stmt` is an `if` whose predicate is not a single plain
7807/// expression: a pattern match (`e matches p`) or an `&&&` chain (§12.6.1).
7808pub fn is_clause_if(stmt: &Statement<'_>) -> bool {
7809    if stmt.kind() != StatementKind::Conditional {
7810        return false;
7811    }
7812    let conditions = stmt.conditions();
7813    conditions.len() != 1 || conditions[0].pattern().is_some()
7814}
7815
7816/// The links an `if` dispatches over: the whole `else if` series of a
7817/// `unique`/`unique0`/`priority` `if`, else the one `if` alone (its `else`,
7818/// if any, is the `otherwise` statement).
7819pub fn if_series<'d>(head: Statement<'d>) -> Result<IfChain<'d>, &'static str> {
7820    if violation_check(&head).is_some() {
7821        return if_chain(head);
7822    }
7823    Ok(IfChain {
7824        links: vec![head],
7825        thens: vec![head.then_branch().ok_or("an `if` with no then-branch")?],
7826        otherwise: head.else_branch(),
7827    })
7828}
7829
7830/// Collects the [`IfChain`] of the `if` statement `head`.
7831pub fn if_chain<'d>(head: Statement<'d>) -> Result<IfChain<'d>, &'static str> {
7832    let mut chain = IfChain {
7833        links: Vec::new(),
7834        thens: Vec::new(),
7835        otherwise: None,
7836    };
7837    let mut link = head;
7838    loop {
7839        chain.links.push(link);
7840        chain
7841            .thens
7842            .push(link.then_branch().ok_or("an `if` with no then-branch")?);
7843        match link.else_branch() {
7844            Some(next)
7845                if next.kind() == StatementKind::Conditional
7846                    && violation_check(&next).is_none() =>
7847            {
7848                link = next;
7849            }
7850            other => {
7851                chain.otherwise = other;
7852                return Ok(chain);
7853            }
7854        }
7855    }
7856}
7857
7858fn first_expression_child<'d>(tc: &TimingControl<'d>) -> Option<Expression<'d>> {
7859    tc.children().into_iter().find_map(|n| n.as_expression())
7860}
7861
7862/// Task 6 (CG-081): the AST-derived read-set of `body` -- every distinct
7863/// (by referenced-symbol identity) `NamedValue` expression `body`'s subtree
7864/// reads, excluding a plain `NamedValue` LHS of a top-level `Assignment`
7865/// (mirrors `sv-sim-codegen::process_lower::external_refs_in_segment`'s
7866/// identical "read vs write position" split, at the granularity of one
7867/// guarded statement rather than a whole `Segment`).
7868///
7869/// **Deliberately AST-only, not `AnalyzedProcedure::
7870/// implicit_event_read_sets()`:** `sv-lang` exposes exactly this oracle
7871/// (`slang::analysis::AnalyzedProcedure::getImplicitEventReadSets`), but it
7872/// needs a live `sv_lang::Analysis` handle, and no `build_cfg` call site in
7873/// this tree (`sv-sim-bin::run`, `aot.rs`, `pipeline.rs`, `frontend.rs`, this
7874/// crate's own `liveness.rs` tests) threads one through today -- adding that
7875/// parameter would be a signature change to `build_cfg` reaching every one
7876/// of those call sites, including `sv-sim-bin::run.rs`, which this wave's
7877/// binding rules forbid risking (wave-B-runtime is concurrently landing
7878/// there). This AST-only walk is a real, if narrower, substitute: an
7879/// over-approximation (a name reached only through a path this walk cannot
7880/// prove is write-only) is a safe, LRM-permitted implicit-sensitivity
7881/// superset (an extra spurious wake, never a missed one) -- never a
7882/// correctness bug the way under-approximating would be. See this file's
7883/// module doc comment's Task 6 section and this task's own deviations for
7884/// the full accounting.
7885fn implicit_comb_read_set<'d>(body: &Statement<'d>) -> Vec<Expression<'d>> {
7886    let mut order = Vec::new();
7887    let mut seen = HashSet::new();
7888    let mut stack = vec![body.as_sem_node()];
7889    while let Some(n) = stack.pop() {
7890        // A subtree slang demoted to `Invalid` inside a statically dead arm
7891        // is invisible to `children`; recover it (its reads are a harmless
7892        // superset of the live ones, LRM 9.4.2.2).
7893        if let Some(st) = n.as_statement()
7894            && st.kind() == StatementKind::Invalid
7895        {
7896            if let Some(c) = st.invalid_child() {
7897                stack.push(c.as_sem_node());
7898            }
7899            continue;
7900        }
7901        if let Some(e) = n.as_expression()
7902            && e.kind() == ExpressionKind::Invalid
7903        {
7904            if let Some(c) = e.invalid_child() {
7905                stack.push(c.as_sem_node());
7906            }
7907            continue;
7908        }
7909        if let Some(e) = n.as_expression() {
7910            if crate::symid::is_value_name(e.kind()) {
7911                if let Some(sym) = e.referenced_symbol()
7912                    && seen.insert(sym.uid())
7913                {
7914                    order.push(e);
7915                }
7916                continue;
7917            }
7918            if e.kind() == ExpressionKind::Assignment {
7919                // Skip only the LHS's own bare `NamedValue` node of a PLAIN
7920                // (`=`/`<=`) assignment (never a "read" for implicit-
7921                // sensitivity purposes, whether blocking or nonblocking) --
7922                // a compound lvalue (a bit-select, concatenation, ...) is
7923                // walked normally instead, so e.g. `arr[i] = x`'s index
7924                // expression `i` (and `arr`'s own identity) are still
7925                // collected, matching the "safe over-approximation" posture
7926                // above.
7927                //
7928                // A COMPOUND assignment (`+=`, `-=`, `&=`, ...) is never
7929                // desugared by slang -- `right()` holds only the RHS
7930                // operand (e.g. `a` for `q += a`), never `q + a` -- so a
7931                // bare `NamedValue` LHS genuinely is also a read there (per
7932                // IEEE 1800's implicit-sensitivity rule: a variable is
7933                // excluded from the inferred `@*` list only when it is
7934                // written but never read, which a compound assignment's LHS
7935                // is not). Detect this via `is_compound_assignment()` and
7936                // walk the LHS unconditionally in that case.
7937                let lhs_is_read = e.is_compound_assignment() == Some(true);
7938                if let Some(lhs) = e.left()
7939                    && (lhs_is_read || !crate::symid::is_value_name(lhs.kind()))
7940                {
7941                    stack.push(lhs.as_sem_node());
7942                }
7943                if let Some(rhs) = e.right() {
7944                    stack.push(rhs.as_sem_node());
7945                }
7946                continue;
7947            }
7948        }
7949        stack.extend(n.children_with_invalid());
7950    }
7951    order
7952}
7953
7954/// LRM 6.14: a chandle "shall not be used ... in sensitivity lists or event
7955/// expressions"; slang does not diagnose it, so refuse it by name here
7956/// instead of waiting on a value that can never be observed to change.
7957fn reject_chandle_event(signal: &Expression<'_>) -> Result<(), LoweringError> {
7958    if signal.expr_type().is_some_and(|t| t.is_chandle()) {
7959        return Err(LoweringError::unsupported(
7960            "a chandle in an event expression (illegal, LRM 1800-2023 6.14)".to_string(),
7961        ));
7962    }
7963    Ok(())
7964}
7965
7966/// The operands `(memory, input_terms, output_terms)` of `stmt` when it is a
7967/// `$async$<and|or|nand|nor>$<array|plane>` call (LRM 20.16), else `None`.
7968pub fn async_pla_call<'d>(stmt: &Statement<'d>) -> Option<Vec<Expression<'d>>> {
7969    let e = stmt.expr()?;
7970    if e.kind() != ExpressionKind::Call || e.is_system_call() != Some(true) {
7971        return None;
7972    }
7973    let name = e.system_subroutine()?.name();
7974    let rest = name.strip_prefix("$async$")?;
7975    let (logic, format) = rest.split_once('$')?;
7976    (matches!(logic, "and" | "or" | "nand" | "nor") && matches!(format, "array" | "plane"))
7977        .then(|| e.operands())
7978}
7979
7980/// What an `$async$...` PLA call re-evaluates on: the personality memory and
7981/// everything the input terms read, never the output terms it writes.
7982fn async_pla_read_set<'d>(stmt: &Statement<'d>) -> Vec<Expression<'d>> {
7983    let ops = async_pla_call(stmt).unwrap_or_default();
7984    let root = |x: Expression<'d>| {
7985        let x = if x.kind() == ExpressionKind::Assignment {
7986            x.left().unwrap_or(x)
7987        } else {
7988            x
7989        };
7990        crate::symid::is_value_name(x.kind())
7991            .then(|| x.referenced_symbol())
7992            .flatten()
7993            .map(|s| (x, s.uid()))
7994    };
7995    let out_uid = ops.get(2).and_then(|o| root(*o)).map(|(_, u)| u);
7996    let mut reads: Vec<Expression<'d>> = implicit_comb_read_set(stmt)
7997        .into_iter()
7998        .filter(|r| r.referenced_symbol().map(|s| s.uid()) != out_uid)
7999        .collect();
8000    if let Some((mem, uid)) = ops.first().and_then(|o| root(*o))
8001        && !reads
8002            .iter()
8003            .any(|r| r.referenced_symbol().map(|s| s.uid()) == Some(uid.clone()))
8004    {
8005        reads.push(mem);
8006    }
8007    reads
8008}
8009
8010/// Task 3 (CG-170): extracts a `SignalEventControl`'s optional `iff`
8011/// condition. **Verified live** against the pinned `sv-lang = "=0.1.9"`
8012/// source (no `.iffCondition` accessor exists in this binding at all): per
8013/// slang's own `SignalEventControl::visitExprs` (`include/slang/ast/
8014/// TimingControl.h`: `expr.visit(visitor); if (iffCondition)
8015/// iffCondition->visit(visitor);`), which is what backs `children()`
8016/// (`CApiAst.cpp`'s generic `SemChildCollector`), a `SignalEvent`
8017/// `TimingControl` node's `children()` returns exactly 1 entry (the event
8018/// expression only) with no `iff`, or exactly 2 (event expression, then
8019/// `iff`, in that order) when one is present. Extraction is therefore
8020/// purely positional: `len() == 2`'s `[1]` is `iff`, `None` otherwise.
8021fn extract_iff<'d>(tc: &TimingControl<'d>) -> Option<Expression<'d>> {
8022    let children = tc.children();
8023    if children.len() == 2 {
8024        children[1].as_expression()
8025    } else {
8026        None
8027    }
8028}
8029
8030/// The pass and fail statements of an immediate assertion when one of them
8031/// waits (LRM 16.3: they are ordinary statements, so `#d`/`@e`/`wait` are
8032/// legal there), else `None` (the assertion stays a straight-line leaf). A
8033/// deferred assertion's action is a subroutine call that is queued (16.4), so
8034/// a waiting one is refused by name.
8035fn suspending_assertion_actions<'d>(
8036    stmt: &Statement<'d>,
8037) -> Option<(Option<Statement<'d>>, Option<Statement<'d>>)> {
8038    let pass = stmt.immediate_assertion_if_true();
8039    let fail = stmt.immediate_assertion_if_false();
8040    let waits = |s: &Option<Statement<'d>>| {
8041        s.as_ref()
8042            .is_some_and(|s| contains_suspend_trigger(s.as_sem_node()))
8043    };
8044    (waits(&pass) || waits(&fail)).then_some((pass, fail))
8045}
8046
8047/// The expression an immediate assertion checks.
8048pub fn assertion_cond<'d>(stmt: &Statement<'d>) -> Option<Expression<'d>> {
8049    stmt.as_sem_node()
8050        .children()
8051        .into_iter()
8052        .find_map(|n| n.as_expression())
8053}
8054
8055/// Whether `stmt` is an `expect` statement (LRM 1800-2023 §16.17): slang models
8056/// it as a concurrent assertion of kind `Expect`.
8057fn is_expect(stmt: &Statement<'_>) -> bool {
8058    stmt.kind() == StatementKind::ConcurrentAssertion
8059        && stmt.assertion_kind() == Some(sv_lang::AssertionKind::Expect)
8060}
8061
8062/// Generic depth-first scan (mirrors `sv-sim-codegen::module::
8063/// reject_unsupported_in_body`'s pattern) for any statement kind
8064/// [`CfgBuilder::walk_stmt`] treats as suspend-triggering, wherever it sits
8065/// inside `node`'s subtree. Used only by the "ordinary straight-line" match
8066/// arm: a construct this task does not decompose (loops, `case`, ...) is
8067/// safe to keep as one opaque leaf `Statement` only as long as none of its
8068/// descendants secretly suspends.
8069fn contains_suspend_trigger(node: SemNode<'_>) -> bool {
8070    contains_suspend_trigger_in(node, &mut std::collections::HashSet::new(), true, true)
8071}
8072
8073/// `contains_suspend_trigger`, also looking through calls to user tasks
8074/// (whose bodies are inlined when they can suspend); `visiting` breaks
8075/// recursion through recursive tasks. `by_request` says whether a
8076/// `p.await();` / `p.suspend();` statement counts: not inside a function
8077/// body, which cannot be inlined as a suspending task (a `suspend()` there
8078/// takes effect at the caller's next blocking point instead).
8079///
8080/// `disable_is_trigger`: whether a `disable label;` counts (it is a jump the
8081/// CFG decomposes, but never itself suspends the process).
8082fn contains_suspend_trigger_in(
8083    node: SemNode<'_>,
8084    visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8085    by_request: bool,
8086    disable_is_trigger: bool,
8087) -> bool {
8088    let mut stack = vec![node];
8089    while let Some(n) = stack.pop() {
8090        if let Some(e) = n.as_expression()
8091            && let Some(callee) = user_task_callee(&e)
8092            && is_suspending_virtual(&callee)
8093        {
8094            return true;
8095        }
8096        if let Some(e) = n.as_expression()
8097            && e.kind() == ExpressionKind::Assignment
8098            && e.assignment_timing().is_some()
8099            && (e.is_nonblocking() != Some(true)
8100                || e.assignment_timing()
8101                    .and_then(|t| t.as_timing_control())
8102                    .is_some_and(|tc| tc.kind() != TimingControlKind::Delay))
8103        {
8104            // `lhs = #d rhs;` / `lhs = @(e) rhs;` / `q <= @(e) rhs;` (LRM 9.4.5,
8105            // 10.4.2): the assignment itself waits (`walk_intra_assignment_timing`,
8106            // `walk_nba_event`), so a task holding one suspends and is inlined --
8107            // otherwise `disable task;` of another process cannot reach it (9.6.2).
8108            return true;
8109        }
8110        if let Some(e) = n.as_expression() {
8111            // An interface `extern` call runs the module-side
8112            // implementations (LRM 25.7): their bodies are what can suspend.
8113            // A call to an `extern forkjoin` task nothing implements is a
8114            // run-time error the CFG reports (`Terminator::Switch`).
8115            let callees: Vec<Symbol<'_>> = match crate::netlist::itf::call_extern_targets(&e) {
8116                Some(t) if t.impls.is_empty() => return true,
8117                Some(t) => t.impls,
8118                None => user_task_callee(&e).into_iter().collect(),
8119            };
8120            for callee in callees {
8121                // A function with an `output`/`inout`/`ref` event formal is
8122                // inlined like a suspending one (`has_event_out_formal`).
8123                if has_event_out_formal(&callee) {
8124                    return true;
8125                }
8126                if let Some(body) = proc_body(&callee)
8127                    && visiting.insert(callee.uid())
8128                    && contains_suspend_trigger_in(
8129                        body.as_sem_node(),
8130                        visiting,
8131                        by_request
8132                            && callee.subroutine_kind() == Some(sv_lang::SubroutineKind::Task),
8133                        // A function is compiled, never inlined: its own
8134                        // `disable blk;` is a branch there (codegen `func`).
8135                        disable_is_trigger
8136                            && callee.subroutine_kind() == Some(sv_lang::SubroutineKind::Task),
8137                    )
8138                {
8139                    return true;
8140                }
8141            }
8142        }
8143        if let Some(s) = n.as_statement() {
8144            let kind = s.kind();
8145            if by_request
8146                && kind == StatementKind::ExpressionStatement
8147                && is_process_by_request_stmt(&s)
8148            {
8149                return true;
8150            }
8151            let is_join_block = kind == StatementKind::Block
8152                && matches!(
8153                    s.block_kind(),
8154                    Some(
8155                        StatementBlockKind::JoinAll
8156                            | StatementBlockKind::JoinAny
8157                            | StatementBlockKind::JoinNone
8158                    )
8159                );
8160            if is_join_block
8161                || is_expect(&s)
8162                || matches!(
8163                    kind,
8164                    StatementKind::Timed
8165                        | StatementKind::Wait
8166                        | StatementKind::WaitFork
8167                        | StatementKind::WaitOrder
8168                        | StatementKind::EventTrigger
8169                        | StatementKind::DisableFork
8170                )
8171                // `disable label;` is a jump in the CFG (see this file's
8172                // module doc comment) -- one hidden inside an opaque
8173                // "ordinary" leaf (e.g. a `case` branch) forces that leaf to
8174                // be decomposed too.
8175                || (disable_is_trigger && kind == StatementKind::Disable)
8176            {
8177                return true;
8178            }
8179            // A static or procedural concurrent assertion (16.14) is evaluated by the
8180            // assertion engine, never by the process that reaches it: a task its
8181            // property calls from a sequence match item (16.11) runs as a thread of
8182            // its own, so it suspends no process here. Only the pass/fail action
8183            // statements run in the hosting process and can hold a suspend point.
8184            if kind == StatementKind::ConcurrentAssertion {
8185                stack.extend(
8186                    live_children(&n)
8187                        .into_iter()
8188                        .filter(|c| c.as_statement().is_some()),
8189                );
8190                continue;
8191            }
8192        }
8193        stack.extend(live_children(&n));
8194    }
8195    false
8196}
8197
8198/// Whether `task` can call itself ([`task_reaches`]), memoized per task in
8199/// `vt` when there is one.
8200fn task_reaches_itself<'a>(task: &Symbol<'a>, vt: Option<&VirtualTasks<'a>>) -> bool {
8201    let uid = task.uid();
8202    if let Some(vt) = vt
8203        && let Some(r) = vt.self_reach.borrow().get(&uid)
8204    {
8205        return *r;
8206    }
8207    let r = task_reaches(task, &uid, vt, &mut std::collections::HashSet::new());
8208    if let Some(vt) = vt {
8209        vt.self_reach.borrow_mut().insert(uid, r);
8210    }
8211    r
8212}
8213
8214/// Whether `sub`'s body calls `target` (directly or through the user tasks
8215/// and functions it calls). A call of a virtual method reaches every
8216/// implementation `vt` knows of (LRM 1800-2023 8.20), so recursion through
8217/// dynamic dispatch (`a.f()` -> override -> `a.f()` on a child) is found.
8218fn task_reaches<'a>(
8219    sub: &Symbol<'a>,
8220    target: &crate::symid::SymbolId,
8221    vt: Option<&VirtualTasks<'a>>,
8222    visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8223) -> bool {
8224    let Some(body) = proc_body(sub) else {
8225        return false;
8226    };
8227    let mut stack = vec![body.as_sem_node()];
8228    while let Some(n) = stack.pop() {
8229        if let Some(e) = n.as_expression()
8230            && let Some(callee) = user_task_callee(&e)
8231        {
8232            let mut callees = vec![callee];
8233            // `super.m()` is not virtual (LRM 8.15): it never reaches the
8234            // overrides, so it must not be seen as recursion through them.
8235            if let Some(vt) = vt
8236                && !is_explicit_super(e)
8237                && (callee
8238                    .subroutine_flags()
8239                    .contains(sv_lang::MethodFlags::VIRTUAL)
8240                    || callee.subroutine_override().is_some())
8241            {
8242                callees.extend(vt.implementations(&callee).into_iter().map(|(_, s)| s));
8243            }
8244            for c in callees {
8245                if c.uid() == *target {
8246                    return true;
8247                }
8248                if visiting.insert(c.uid()) && task_reaches(&c, target, vt, visiting) {
8249                    return true;
8250                }
8251            }
8252        }
8253        stack.extend(live_children(&n));
8254    }
8255    false
8256}
8257
8258/// Whether `node`'s subtree calls a subroutine that is not a system task.
8259fn contains_subroutine_call(node: SemNode<'_>) -> bool {
8260    let mut stack = vec![node];
8261    while let Some(n) = stack.pop() {
8262        if let Some(e) = n.as_expression()
8263            && e.kind() == ExpressionKind::Call
8264            && e.is_system_call() != Some(true)
8265        {
8266            return true;
8267        }
8268        stack.extend(live_children(&n));
8269    }
8270    false
8271}
8272
8273/// The callee of a call to a user (non-system, non-DPI) task or function.
8274fn user_task_callee<'d>(e: &Expression<'d>) -> Option<Symbol<'d>> {
8275    if e.kind() != ExpressionKind::Call || e.is_system_call() == Some(true) {
8276        return None;
8277    }
8278    let callee = e.call_subroutine()?;
8279    if callee
8280        .subroutine_flags()
8281        .contains(sv_lang::MethodFlags::DPI_IMPORT)
8282    {
8283        return None;
8284    }
8285    Some(callee)
8286}
8287
8288/// The callee of `e` when it is a call to a user task whose body can
8289/// suspend -- such a call is inlined into the calling process's CFG.
8290fn suspending_task_callee<'d>(e: &Expression<'d>) -> Option<Symbol<'d>> {
8291    let callee = user_task_callee(e)?;
8292    subroutine_can_suspend(&callee).then_some(callee)
8293}
8294
8295/// The calls to user functions that can suspend (a `fork` in the body)
8296/// under `node`, innermost (argument) calls first.
8297fn collect_suspending_function_calls<'d>(node: SemNode<'d>, out: &mut Vec<Expression<'d>>) {
8298    for c in node.children() {
8299        collect_suspending_function_calls(c, out);
8300    }
8301    if let Some(e) = node.as_expression()
8302        && let Some(callee) = user_task_callee(&e)
8303        && callee.subroutine_kind() == Some(sv_lang::SubroutineKind::Function)
8304        && subroutine_can_suspend(&callee)
8305    {
8306        out.push(e);
8307    }
8308}
8309
8310/// Whether `sub` has an `output`/`inout`/`ref` `event` formal: its handle is
8311/// copied back to the actual (LRM 1800-2023 15.5.5.1), which only an inlined
8312/// body can do, so every call to it is inlined (value functions through
8313/// `hoist_suspending_function_calls`).
8314fn has_event_out_formal(sub: &Symbol<'_>) -> bool {
8315    sub.subroutine_arguments().any(|a| {
8316        a.formal_argument_direction() != sv_lang::ArgumentDirection::In
8317            && a.value_type().is_some_and(|t| t.is_event())
8318    })
8319}
8320
8321/// The key under which the code generator finds the capture temporary that
8322/// holds the value of the inlined call `call` ([`ArgBinding::CallResult`]).
8323pub fn call_result_key(call: &Expression<'_>) -> crate::symid::SymbolId {
8324    let at = call.source_range().start;
8325    crate::symid::SymbolId::synthetic_tagged(&format!("callret.{}.{}", at.buffer, at.offset))
8326}
8327
8328/// Whether `sub`'s body (or a task it calls) contains a suspend point --
8329/// every call to such a task is inlined into the calling process's CFG
8330/// instead of being compiled as a function.
8331pub fn subroutine_can_suspend(sub: &Symbol<'_>) -> bool {
8332    if is_suspending_virtual(sub) {
8333        return true;
8334    }
8335    let Some(body) = proc_body(sub) else {
8336        return false;
8337    };
8338    // An `output`/`inout`/`ref` event formal is copied back as a handle
8339    // (LRM 1800-2023 15.5.5.1), which only an inlined task can do.
8340    if has_event_out_formal(sub) {
8341        return true;
8342    }
8343    let mut visiting = std::collections::HashSet::new();
8344    visiting.insert(sub.uid());
8345    let by_request = sub.subroutine_kind() == Some(sv_lang::SubroutineKind::Task);
8346    contains_suspend_trigger_in(body.as_sem_node(), &mut visiting, by_request, by_request)
8347        || (sub.subroutine_kind() == Some(sv_lang::SubroutineKind::Task)
8348            && task_stages_nba(sub, &mut std::collections::HashSet::new()))
8349}
8350
8351/// Whether the task `sub` (or a task it calls) runs a nonblocking assignment
8352/// (LRM 1800-2023 10.4.2). Staging an NBA needs the calling process's
8353/// scheduler handle, which a compiled function does not have, so such a task
8354/// is inlined into each calling process like a suspending one -- it just
8355/// never reaches a suspend point.
8356fn task_stages_nba(
8357    sub: &Symbol<'_>,
8358    visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8359) -> bool {
8360    let Some(body) = proc_body(sub) else {
8361        return false;
8362    };
8363    let mut stack = vec![body.as_sem_node()];
8364    while let Some(n) = stack.pop() {
8365        if let Some(e) = n.as_expression() {
8366            if e.kind() == ExpressionKind::Assignment && e.is_nonblocking() == Some(true) {
8367                return true;
8368            }
8369            if let Some(callee) = user_task_callee(&e)
8370                && callee.subroutine_kind() == Some(sv_lang::SubroutineKind::Task)
8371                && visiting.insert(callee.uid())
8372                && task_stages_nba(&callee, visiting)
8373            {
8374                return true;
8375            }
8376        }
8377        stack.extend(live_children(&n));
8378    }
8379    false
8380}
8381
8382/// The method name when `callee` is a method of the built-in `std::process`
8383/// class (LRM 9.7): the class named `process` whose scope chain reaches the
8384/// `std` package -- scope identity, as `sv-sim-codegen`'s
8385/// `is_builtin_std_class` does (a user class `process` is not it).
8386pub fn std_process_method(callee: &Symbol<'_>) -> Option<String> {
8387    std_class_method(callee, "process")
8388}
8389
8390/// The method name when `callee` is a method of the built-in `std` package
8391/// class named `class_name` (`process`, `semaphore`, `mailbox`): scope
8392/// identity, so a user class of the same name is not it.
8393pub fn std_class_method(callee: &Symbol<'_>, class_name: &str) -> Option<String> {
8394    let class = callee.parent()?;
8395    if !matches!(
8396        class.kind(),
8397        SymbolKind::ClassType | SymbolKind::GenericClassDef
8398    ) || class.name() != class_name
8399    {
8400        return None;
8401    }
8402    let mut scope = class.parent();
8403    while let Some(s) = scope {
8404        if s.kind() == SymbolKind::Package && s.name() == "std" {
8405            return Some(callee.name().to_string());
8406        }
8407        scope = s.parent();
8408    }
8409    None
8410}
8411
8412/// Whether event expression `e` names a CLASS event (LRM 1800-2023 15.5): an
8413/// instance `event` property (`m_event`, `obj.m_event`, `h[k].raised`), or an
8414/// `event` local or formal declared inside a class method. Such an event is a
8415/// runtime object (`sched::class_event`), not a row of a module's event pool.
8416/// Conversions are looked through; `null` is no class event by itself.
8417pub fn is_class_event_expr(e: &Expression<'_>) -> bool {
8418    let mut e = *e;
8419    while e.kind() == ExpressionKind::Conversion
8420        && let Some(inner) = e.conversion_operand()
8421    {
8422        e = inner;
8423    }
8424    if !e.expr_type().is_some_and(|t| t.is_event()) {
8425        return false;
8426    }
8427    let sym = if e.kind() == ExpressionKind::MemberAccess {
8428        e.member_symbol()
8429    } else if matches!(
8430        e.kind(),
8431        ExpressionKind::NamedValue | ExpressionKind::HierarchicalValue
8432    ) {
8433        e.referenced_symbol()
8434    } else {
8435        None
8436    };
8437    let Some(sym) = sym else {
8438        return false;
8439    };
8440    if sym.kind() == SymbolKind::ClassProperty {
8441        return true;
8442    }
8443    // A local/formal: the nearest enclosing subroutine is a class method.
8444    let mut scope = sym.parent();
8445    while let Some(s) = scope {
8446        if s.kind() == SymbolKind::Subroutine {
8447            return s.parent().is_some_and(|p| {
8448                matches!(
8449                    p.kind(),
8450                    SymbolKind::ClassType | SymbolKind::GenericClassDef
8451                )
8452            });
8453        }
8454        if matches!(
8455            s.kind(),
8456            SymbolKind::ClassType | SymbolKind::GenericClassDef
8457        ) {
8458            return false;
8459        }
8460        scope = s.parent();
8461    }
8462    false
8463}
8464
8465/// The class events `tc` waits on when it is `@e` on a class event or an
8466/// `@(e1 or e2 ...)` list with class events (no edge, no `iff` on a class
8467/// term): waits that queue through the runtime ([`is_class_event_expr`]).
8468/// Empty when `tc` waits on no class event. A list that also has signal or
8469/// module-event terms is [`class_event_wait_is_mixed`]; an edge or `iff` on a
8470/// class term is a named error.
8471pub fn class_event_wait_terms<'d>(
8472    tc: &TimingControl<'d>,
8473) -> Result<Vec<Expression<'d>>, LoweringError> {
8474    let kind = tc.kind();
8475    if kind == TimingControlKind::SignalEvent {
8476        let Some(sig) = first_expression_child(tc) else {
8477            return Ok(Vec::new());
8478        };
8479        if !is_class_event_expr(&sig) {
8480            return Ok(Vec::new());
8481        }
8482        if tc.edge() != EdgeKind::None {
8483            return Err(LoweringError::unsupported(
8484                "an edge event control on a class `event` (LRM 1800-2023 15.5)",
8485            ));
8486        }
8487        return Ok(vec![sig]);
8488    }
8489    if kind == TimingControlKind::EventList {
8490        let mut terms = Vec::new();
8491        for child in tc.children() {
8492            let Some(sub) = child.as_timing_control() else {
8493                continue;
8494            };
8495            match first_expression_child(&sub) {
8496                Some(sig)
8497                    if sub.kind() == TimingControlKind::SignalEvent
8498                        && is_class_event_expr(&sig) =>
8499                {
8500                    if sub.edge() != EdgeKind::None || extract_iff(&sub).is_some() {
8501                        return Err(LoweringError::unsupported(
8502                            "an edge or `iff` term on a class `event` in an `@(a or b)` event list (LRM 1800-2023 15.5)",
8503                        ));
8504                    }
8505                    terms.push(sig);
8506                }
8507                _ => {}
8508            }
8509        }
8510        return Ok(terms);
8511    }
8512    Ok(Vec::new())
8513}
8514
8515/// Whether `tc` is an `@(a or b)` event list that mixes class `event` terms
8516/// with signal or module-event terms (LRM 1800-2023 9.4.2.1, 15.5): such a
8517/// wait is both a class-event queue entry and a sensitivity-table wait.
8518pub fn class_event_wait_is_mixed(tc: &TimingControl<'_>) -> bool {
8519    if tc.kind() != TimingControlKind::EventList {
8520        return false;
8521    }
8522    let (mut class, mut plain) = (false, false);
8523    for child in tc.children() {
8524        let Some(sub) = child.as_timing_control() else {
8525            continue;
8526        };
8527        match first_expression_child(&sub) {
8528            Some(sig)
8529                if sub.kind() == TimingControlKind::SignalEvent && is_class_event_expr(&sig) =>
8530            {
8531                class = true;
8532            }
8533            _ => plain = true,
8534        }
8535    }
8536    class && plain
8537}
8538
8539/// Whether `stmt` is `p.await();`, `p.suspend();`, a blocking
8540/// `semaphore.get(...)` or a mailbox `put(...)`/`get(...)`/`peek(...)` as a
8541/// statement of its own (not nested in a larger expression): the `std` calls
8542/// that are a suspend point, because the caller may stop there.
8543fn is_process_by_request_stmt(stmt: &Statement<'_>) -> bool {
8544    let Some(e) = stmt.expr() else {
8545        return false;
8546    };
8547    if e.kind() != ExpressionKind::Call || e.is_system_call() == Some(true) {
8548        return false;
8549    }
8550    let Some(callee) = e.call_subroutine() else {
8551        return false;
8552    };
8553    std_process_method(&callee).is_some_and(|m| m == "await" || m == "suspend")
8554        || std_class_method(&callee, "semaphore").is_some_and(|m| m == "get")
8555        || std_class_method(&callee, "mailbox")
8556            .is_some_and(|m| matches!(m.as_str(), "put" | "get" | "peek"))
8557}
8558
8559/// Whether `stmt` is a mailbox `get(x);` / `peek(x);`: the blocking calls whose
8560/// result is delivered after the process resumes (the CFG emits the statement
8561/// again after its suspend point). A `put` finishes in its one call.
8562fn is_mailbox_two_phase_stmt(stmt: &Statement<'_>) -> bool {
8563    let Some(e) = stmt.expr() else {
8564        return false;
8565    };
8566    e.kind() == ExpressionKind::Call
8567        && e.is_system_call() != Some(true)
8568        && e.call_subroutine()
8569            .and_then(|callee| std_class_method(&callee, "mailbox"))
8570            .is_some_and(|m| m == "get" || m == "peek")
8571}
8572
8573/// Whether a call to `sub` can end the calling process from inside it: its
8574/// body, or a subroutine it calls, runs `$finish`/`$stop`/`$fatal` or a
8575/// task-form `$cast` that can fail, uses a
8576/// virtual interface (a `null` one is a fatal error, LRM 1800-2023 §25.9),
8577/// or calls a DPI import (which may call back into an export that does).
8578/// A call to a virtual method counts if ANY implementation in its override
8579/// chain can abort ([`mark_aborting_virtual`]: which override runs is only
8580/// known at run time), and a `new` expression counts if its constructor
8581/// chain or a property initializer can ([`class_construction_can_abort`]).
8582pub fn subroutine_can_abort(sub: &Symbol<'_>) -> bool {
8583    let mut visiting = std::collections::HashSet::new();
8584    can_abort_in(sub, &mut visiting)
8585}
8586
8587thread_local! {
8588    /// The uid of every virtual-method symbol (a base declaration, each
8589    /// override, an interface-class prototype) in a vtable slot at least one
8590    /// of whose implementations can abort. A call to a virtual method
8591    /// resolves statically to the BASE declaration, whose own body is empty,
8592    /// so [`can_abort_in`] cannot learn about an aborting override from the
8593    /// callee's body: codegen computes this set once, to a fixed point,
8594    /// before lowering any call site ([`mark_aborting_virtual`]).
8595    static ABORTING_VIRTUALS: std::cell::RefCell<HashSet<crate::symid::SymbolId>> =
8596        std::cell::RefCell::new(HashSet::new());
8597    /// Whether a `randomize()` call can end the process: some class's
8598    /// `pre_randomize()`/`post_randomize()` hook (which `randomize()` runs
8599    /// on the receiver or on any object it reaches, chosen by dynamic class)
8600    /// can abort ([`set_randomize_can_abort`]).
8601    static RANDOMIZE_CAN_ABORT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
8602}
8603
8604/// Empties the aborting-virtual set (and the `randomize()` flag); codegen
8605/// calls this before building a design's vtables.
8606pub fn reset_aborting_virtuals() {
8607    ABORTING_VIRTUALS.with(|s| s.borrow_mut().clear());
8608    RANDOMIZE_CAN_ABORT.with(|c| c.set(false));
8609}
8610
8611/// Declares that `randomize()` calls can abort (a `pre_randomize()` or
8612/// `post_randomize()` hook of the design can). Returns whether this changed the
8613/// flag, for a caller iterating to a fixed point.
8614pub fn set_randomize_can_abort() -> bool {
8615    RANDOMIZE_CAN_ABORT.with(|c| !c.replace(true))
8616}
8617
8618/// Whether [`set_randomize_can_abort`] has been called since the last reset.
8619pub fn randomize_can_abort() -> bool {
8620    RANDOMIZE_CAN_ABORT.with(|c| c.get())
8621}
8622
8623/// Records `uid` (a virtual method belonging to a slot with an aborting
8624/// implementation) as one whose calls can abort. Returns whether it was newly
8625/// recorded, so a caller iterating to a fixed point knows when it is done.
8626pub fn mark_aborting_virtual(uid: crate::symid::SymbolId) -> bool {
8627    ABORTING_VIRTUALS.with(|s| s.borrow_mut().insert(uid))
8628}
8629
8630fn can_abort_in(
8631    sub: &Symbol<'_>,
8632    visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8633) -> bool {
8634    if sub
8635        .subroutine_flags()
8636        .contains(sv_lang::MethodFlags::DPI_IMPORT)
8637    {
8638        return true;
8639    }
8640    if ABORTING_VIRTUALS.with(|s| s.borrow().contains(&sub.uid())) {
8641        return true;
8642    }
8643    if !visiting.insert(sub.uid()) {
8644        return false;
8645    }
8646    let Some(body) = proc_body(sub) else {
8647        return false;
8648    };
8649    if node_can_abort(body.as_sem_node(), Some(sub), visiting) {
8650        return true;
8651    }
8652    // A constructor's class-level work (property initializers, the
8653    // `extends Base(args)` call) runs with it, as part of the `new` that
8654    // invokes it.
8655    sub.subroutine_flags()
8656        .contains(sv_lang::MethodFlags::CONSTRUCTOR)
8657        && sub
8658            .parent()
8659            .and_then(|class| class.as_type())
8660            .is_some_and(|ty| class_level_can_abort(ty, visiting))
8661}
8662
8663/// Whether constructing an object of class `ty` (`new`: every constructor
8664/// from the root base class down, every level's property initializers and
8665/// `extends Base(args)` call) can end the process.
8666pub fn class_construction_can_abort(ty: sv_lang::Type<'_>) -> bool {
8667    let mut visiting = std::collections::HashSet::new();
8668    construction_can_abort(ty, &mut visiting)
8669}
8670
8671fn construction_can_abort(
8672    ty: sv_lang::Type<'_>,
8673    visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8674) -> bool {
8675    let mut level = Some(ty);
8676    while let Some(t) = level {
8677        if !t.is_class() {
8678            return false;
8679        }
8680        if class_level_can_abort(t, visiting) {
8681            return true;
8682        }
8683        if t.class_constructor()
8684            .is_some_and(|ctor| can_abort_in(&ctor, visiting))
8685        {
8686            return true;
8687        }
8688        level = t.class_base();
8689    }
8690    false
8691}
8692
8693/// Whether the work one class level does around its constructor body can
8694/// end the process: its property initializers and its `extends Base(args)`
8695/// call (the explicit-`super.new` form is part of the constructor body).
8696fn class_level_can_abort(
8697    ty: sv_lang::Type<'_>,
8698    visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8699) -> bool {
8700    let class_sym = ty.as_symbol();
8701    // `class A; A next = new; endclass` constructs itself: the guard keeps
8702    // the analysis finite (a class level is recorded once, under its own uid).
8703    if !visiting.insert(class_sym.uid()) {
8704        return false;
8705    }
8706    if let Some(call) = ty.class_base_constructor_call()
8707        && node_can_abort(call.as_sem_node(), None, visiting)
8708    {
8709        return true;
8710    }
8711    for member in class_sym.members() {
8712        if member.kind() == SymbolKind::ClassProperty
8713            && let Some(init) = member.initializer()
8714            && node_can_abort(init.as_sem_node(), None, visiting)
8715        {
8716            return true;
8717        }
8718    }
8719    false
8720}
8721
8722/// [`can_abort_in`]'s walk over one subtree. `enclosing` is the subroutine
8723/// the subtree belongs to (a `super.new(...)` finds its base class through
8724/// it).
8725fn node_can_abort<'d>(
8726    root: SemNode<'d>,
8727    enclosing: Option<&Symbol<'d>>,
8728    visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8729) -> bool {
8730    let mut stack = vec![root];
8731    while let Some(n) = stack.pop() {
8732        // `$cast(dest, src);` as a task is a run-time error when it fails
8733        // (LRM 6.24.2), ending the process like `$fatal`.
8734        if let Some(s) = n.as_statement()
8735            && s.kind() == StatementKind::ExpressionStatement
8736            && let Some(e) = s.expr()
8737            && is_failable_task_cast(&e)
8738        {
8739            return true;
8740        }
8741        if let Some(e) = n.as_expression() {
8742            if crate::netlist::vif::is_member_access(&e)
8743                || crate::netlist::vif::call_targets(&e).is_some()
8744            {
8745                return true;
8746            }
8747            if e.kind() == ExpressionKind::Call {
8748                if e.is_system_call() == Some(true) {
8749                    if e.system_subroutine().is_some_and(|s| {
8750                        match s.known_name_id() {
8751                            KnownSystemName::Finish
8752                            | KnownSystemName::Stop
8753                            | KnownSystemName::Fatal => true,
8754                            // `obj.randomize()` runs the class's
8755                            // `pre_/post_randomize()` hooks (LRM 18.6.2).
8756                            KnownSystemName::Randomize => randomize_can_abort(),
8757                            _ => false,
8758                        }
8759                    }) {
8760                        return true;
8761                    }
8762                } else if let Some(callee) = e.call_subroutine() {
8763                    // `p.kill()` ends the caller when `p` is the caller (or
8764                    // one of its ancestors), which is only known at run time.
8765                    if std_process_method(&callee).is_some_and(|m| m == "kill") {
8766                        return true;
8767                    }
8768                    // An interface `extern` call runs its implementations.
8769                    for target in crate::netlist::itf::call_callees(&e) {
8770                        if can_abort_in(&target, visiting) {
8771                            return true;
8772                        }
8773                    }
8774                }
8775            } else if e.kind() == ExpressionKind::NewClass {
8776                if let Some(ctor_call) = e.new_class_constructor_call() {
8777                    stack.push(ctor_call.as_sem_node());
8778                }
8779                // `super.new(...)`'s expression type is `void`: the class
8780                // whose construction it continues is the enclosing
8781                // constructor's base. Any other `new` constructs its type.
8782                let constructed = if e.new_class_is_super_class() == Some(true) {
8783                    enclosing
8784                        .and_then(|sub| sub.parent())
8785                        .and_then(|class| class.as_type())
8786                        .and_then(|ty| ty.class_base())
8787                } else {
8788                    e.expr_type().map(|t| t.canonical())
8789                };
8790                if constructed.is_some_and(|ty| construction_can_abort(ty, visiting)) {
8791                    return true;
8792                }
8793            }
8794        }
8795        stack.extend(live_children(&n));
8796    }
8797    false
8798}
8799
8800/// Whether `e` is the task form of `$cast` whose destination type can reject
8801/// a value (a class handle or an enum; any other destination accepts every
8802/// source, so the call can never be the run-time error of LRM 6.24.2).
8803fn is_failable_task_cast(e: &Expression<'_>) -> bool {
8804    if e.kind() != ExpressionKind::Call
8805        || e.is_system_call() != Some(true)
8806        || !e
8807            .system_subroutine()
8808            .is_some_and(|s| s.known_name_id() == KnownSystemName::Cast)
8809    {
8810        return false;
8811    }
8812    let ops = e.operands();
8813    let Some(&dest) = ops.first() else {
8814        return false;
8815    };
8816    let dest = if dest.kind() == ExpressionKind::Assignment {
8817        dest.left().unwrap_or(dest)
8818    } else {
8819        dest
8820    };
8821    dest.expr_type()
8822        .is_some_and(|t| t.is_class() || t.is_enum())
8823}
8824
8825/// The target of an `output`/`inout` call argument: slang binds it as
8826/// `actual = <formal>`.
8827fn call_actual_lvalue<'d>(actual: Expression<'d>) -> Expression<'d> {
8828    if actual.kind() == ExpressionKind::Assignment {
8829        actual.left().unwrap_or(actual)
8830    } else {
8831        actual
8832    }
8833}
8834
8835/// The variables of the activation that forks `branch` that the branch
8836/// uses: automatic variables and inlined-task formals it does not declare
8837/// itself (static ones are the instance's signals).
8838fn captured_vars<'d>(branch: &ProcessCfg<'d>) -> Vec<Symbol<'d>> {
8839    let own: std::collections::HashSet<crate::symid::SymbolId> =
8840        super::liveness::declared_local_ids(branch);
8841    captured_in_excluding(vec![branch.body.as_sem_node()], &own)
8842}
8843
8844/// [`captured_vars`] over explicit AST roots, for a branch that declares nothing.
8845fn captured_in<'d>(roots: Vec<sv_lang::SemNode<'d>>) -> Vec<Symbol<'d>> {
8846    captured_in_excluding(roots, &std::collections::HashSet::new())
8847}
8848
8849fn captured_in_excluding<'d>(
8850    roots: Vec<sv_lang::SemNode<'d>>,
8851    own: &std::collections::HashSet<crate::symid::SymbolId>,
8852) -> Vec<Symbol<'d>> {
8853    let mut out: Vec<Symbol<'d>> = Vec::new();
8854    let mut seen = std::collections::HashSet::new();
8855    let mut stack = roots;
8856    while let Some(n) = stack.pop() {
8857        if let Some(e) = n.as_expression()
8858            && crate::symid::is_value_name(e.kind())
8859            && let Some(r) = e.referenced_symbol()
8860        {
8861            let procedural = (r.kind() == SymbolKind::Variable
8862                && r.variable_lifetime() == sv_lang::VariableLifetime::Automatic
8863                && r.parent().is_some_and(|p| {
8864                    matches!(
8865                        p.kind(),
8866                        SymbolKind::StatementBlock | SymbolKind::Subroutine
8867                    )
8868                }))
8869                || r.kind() == SymbolKind::FormalArgument;
8870            if procedural && !own.contains(&r.uid()) && seen.insert(r.uid()) {
8871                out.push(r);
8872            }
8873        }
8874        stack.extend(live_children(&n));
8875    }
8876    out
8877}
8878
8879/// The automatic variables the block statement `stmt` declares: created and
8880/// initialized each time the block is entered (LRM 1800-2023 §6.21), in
8881/// declaration order. A `for (int i = ...)` loop's variables live in the
8882/// implicit block around it, entered as the loop starts; with
8883/// `with_loop_vars` false they are left out (for a lowering that sets them
8884/// as part of the loop).
8885pub fn block_automatic_vars<'d>(stmt: &Statement<'d>, with_loop_vars: bool) -> Vec<Symbol<'d>> {
8886    if stmt.kind() != StatementKind::Block {
8887        return Vec::new();
8888    }
8889    let Some(block) = stmt.block_symbol() else {
8890        return Vec::new();
8891    };
8892    let mut loop_vars = std::collections::HashSet::new();
8893    if !with_loop_vars {
8894        let mut stack = stmt.statements();
8895        while let Some(s) = stack.pop() {
8896            if s.kind() == StatementKind::List {
8897                stack.extend(s.statements());
8898            } else if s.kind() == StatementKind::ForLoop {
8899                loop_vars.extend(s.for_loop_vars().iter().map(|v| v.uid()));
8900            }
8901        }
8902    }
8903    block
8904        .members()
8905        .filter(|m| {
8906            m.kind() == SymbolKind::Variable
8907                && m.variable_lifetime() == sv_lang::VariableLifetime::Automatic
8908                && !loop_vars.contains(&m.uid())
8909        })
8910        .collect()
8911}
8912
8913/// The prefix of the names sv-sim gives the blocks it introduces into a
8914/// design (`randsequence`'s expansion): they are not part of any
8915/// hierarchical name the design prints.
8916pub const INTERNAL_NAME_PREFIX: &str = "__sv_sim_";
8917
8918/// `sym`'s hierarchical name as the design sees it (`%m`, a message's
8919/// scope): the blocks sv-sim introduced ([`INTERNAL_NAME_PREFIX`]) left out.
8920pub fn shown_path(sym: &Symbol<'_>) -> String {
8921    let path = sym.hierarchical_path();
8922    if !path.contains(INTERNAL_NAME_PREFIX) {
8923        return path;
8924    }
8925    let mut out = String::with_capacity(path.len());
8926    let mut rest = path.as_str();
8927    while let Some(i) = rest.find(INTERNAL_NAME_PREFIX) {
8928        let at_segment = i == 0 || rest.as_bytes()[i - 1] == b'.';
8929        if !at_segment {
8930            out.push_str(&rest[..i + INTERNAL_NAME_PREFIX.len()]);
8931            rest = &rest[i + INTERNAL_NAME_PREFIX.len()..];
8932            continue;
8933        }
8934        // Drop the segment and the `.` before it (or after it, at the start).
8935        let end = rest[i..].find('.').map_or(rest.len(), |e| i + e);
8936        if i > 0 {
8937            out.push_str(&rest[..i - 1]);
8938            rest = &rest[end..];
8939        } else {
8940            rest = rest[end..].strip_prefix('.').unwrap_or(&rest[end..]);
8941        }
8942    }
8943    out.push_str(rest);
8944    out
8945}
8946
8947/// Whether a variable of type `ty` can hold a class handle or a queue /
8948/// dynamic / associative array handle (directly or in a struct field): the
8949/// variables whose block exit owes a release. A fixed-size array of handles
8950/// is not covered (its elements are released on overwrite only).
8951pub fn may_hold_handles(ty: &sv_lang::Type<'_>) -> bool {
8952    if ty.is_class() || crate::state::storage::is_collection(ty) {
8953        return true;
8954    }
8955    ty.is_unpacked_struct()
8956        && ty
8957            .fields()
8958            .any(|f| f.value_type().is_some_and(|t| may_hold_handles(&t)))
8959}
8960
8961/// Whether `var` is an automatic variable a block declares (initialized on
8962/// entry to that block, [`block_automatic_vars`]).
8963pub fn is_automatic_block_var(var: &Symbol<'_>) -> bool {
8964    var.variable_lifetime() == sv_lang::VariableLifetime::Automatic
8965        && var
8966            .parent()
8967            .is_some_and(|p| p.kind() == SymbolKind::StatementBlock)
8968}
8969
8970/// A task's own variables: members of the task scope (not its formals or
8971/// result variable) and of every block inside its body.
8972fn task_locals<'d>(task: &Symbol<'d>) -> Vec<Symbol<'d>> {
8973    let formal_ids: std::collections::HashSet<crate::symid::SymbolId> =
8974        task.subroutine_arguments().map(|a| a.uid()).collect();
8975    let ret_id = task.subroutine_return_val_var().map(|r| r.uid());
8976    let mut out = Vec::new();
8977    let mut seen = std::collections::HashSet::new();
8978    let mut take = |m: Symbol<'d>, out: &mut Vec<Symbol<'d>>| {
8979        let id = m.uid();
8980        if m.kind() == SymbolKind::Variable
8981            && !formal_ids.contains(&id)
8982            && Some(&id) != ret_id.as_ref()
8983            && seen.insert(id)
8984        {
8985            out.push(m);
8986        }
8987    };
8988    for m in task.members() {
8989        take(m, &mut out);
8990    }
8991    if let Some(body) = proc_body(task) {
8992        let mut stack = vec![body.as_sem_node()];
8993        while let Some(n) = stack.pop() {
8994            if let Some(s) = n.as_statement() {
8995                for block in block_scopes(&s) {
8996                    for m in block.members() {
8997                        take(m, &mut out);
8998                    }
8999                }
9000            }
9001            let mut kids = live_children(&n);
9002            kids.reverse();
9003            stack.extend(kids);
9004        }
9005    }
9006    out
9007}
9008
9009// --- Segment IR (PR-041, PR-042, PR-043; SegId numbering CG-098, CG-099) ---
9010
9011/// Task S3b's real liveness/frame-layout shape now lives in
9012/// `process::liveness` (`FrameLayout`/`FrameSlotDesc`/`FrameSlotSource`) --
9013/// re-exported here under this task's original name so every existing call
9014/// site (this file's own `lower_to_segments`, `tests/process_cfg.rs`) keeps
9015/// working unchanged.
9016pub use super::liveness::FrameLayout;
9017use super::liveness::FrameSlotSource;
9018
9019/// The codegen-facing, already-liveness-annotated lowering shape (PR-042).
9020/// `live_in`/`live_out` are `FrameLayout::slots` indices (Task S3b: see
9021/// `lower_to_segments`'s doc comment for exactly which slots those are).
9022#[derive(Debug, Clone)]
9023pub struct Segment<'d> {
9024    pub id: SegId,
9025    pub live_in: Vec<u32>,
9026    /// Mirrors [`BasicBlock::bindings`] 1:1 -- run before `body`.
9027    pub bindings: Vec<ArgBinding<'d>>,
9028    /// Mirrors [`BasicBlock::this_var`] 1:1.
9029    pub this_var: Option<Symbol<'d>>,
9030    /// Mirrors [`BasicBlock::ref_aliases`] 1:1.
9031    pub ref_aliases: Vec<(Symbol<'d>, Symbol<'d>)>,
9032    pub body: Vec<Statement<'d>>,
9033    /// Mirrors [`BasicBlock::probes`] 1:1 -- indices are into `body`.
9034    pub probes: Vec<(usize, Statement<'d>)>,
9035    /// Mirrors [`BasicBlock::disable_probes`] 1:1 -- indices are into `body`.
9036    pub disable_probes: Vec<(usize, Statement<'d>)>,
9037    /// Mirrors [`BasicBlock::frame_probes`] 1:1 -- indices are into `body`.
9038    pub frame_probes: Vec<(usize, FrameProbe<'d>)>,
9039    /// Mirrors [`BasicBlock::block_events`] 1:1 -- indices are into `body`.
9040    pub block_events: Vec<(usize, Symbol<'d>, bool)>,
9041    /// Mirrors [`BasicBlock::stmt_scopes`] 1:1 -- indices are into `body`.
9042    pub stmt_scopes: Vec<(usize, Symbol<'d>)>,
9043    /// Mirrors [`BasicBlock::step_exprs`] 1:1 -- see its doc comment.
9044    pub step_exprs: Vec<Expression<'d>>,
9045    /// Mirrors [`BasicBlock::repeat_init`] 1:1 -- see its doc comment.
9046    pub repeat_init: Option<(RepeatCounterId, RepeatCount<'d>)>,
9047    /// Mirrors [`BasicBlock::repeat_decrement`] 1:1 -- see its doc comment.
9048    pub repeat_decrement: Option<RepeatCounterId>,
9049    /// Mirrors [`BasicBlock::expect_resume`] 1:1 -- see its doc comment.
9050    pub expect_resume: bool,
9051    pub terminator: SegTerminator<'d>,
9052}
9053
9054/// PR-043: covers every [`Terminator`] variant with no lossy fallback (see
9055/// [`lower_to_segments`]'s 1:1 mapping).
9056#[derive(Debug, Clone)]
9057pub enum SegTerminator<'d> {
9058    Goto(SegId),
9059    CondGoto {
9060        cond: Expression<'d>,
9061        t: SegId,
9062        f: SegId,
9063    },
9064    Suspend {
9065        point: SuspendPoint<'d>,
9066        resume: SegId,
9067        live_out: Vec<u32>,
9068    },
9069    End {
9070        loop_back: Option<SegId>,
9071    },
9072    /// The lowering of [`Terminator::Switch`].
9073    SwitchGoto {
9074        on: SwitchOn<'d>,
9075        arms: Vec<SegId>,
9076        default: SegId,
9077        report: Option<sv_lang::SourceLoc>,
9078    },
9079    /// R-run Task 8: the lossless lowering of [`Terminator::RepeatCheck`] --
9080    /// see that variant's own doc comment for why a `repeat(N)` loop's
9081    /// per-iteration decision cannot be represented as an ordinary
9082    /// `CondGoto` (no `cond: Expression` exists for "compiler-synthesized
9083    /// counter != 0"). **Deviation from this file's own module doc comment**
9084    /// (Task 1's "frozen, codegen-exhausted `SegTerminator`" policy): that
9085    /// policy was written to rule out `ForkJoin` specifically (which has no
9086    /// honest `SegTerminator` shape at all, ever, this wave); a suspend
9087    /// nested inside a `repeat` body is a genuinely new kind of branch
9088    /// condition this crate had no way to construct before this task, not a
9089    /// reachability gap in the pre-existing four variants -- recorded in
9090    /// this task's own deviations.
9091    CondGotoCounter {
9092        counter: RepeatCounterId,
9093        t: SegId,
9094        f: SegId,
9095    },
9096    /// R-run Task 17 (PR-047/PR-061): the lossless lowering of
9097    /// [`Terminator::ForkJoin`] -- a real, new `SegTerminator` shape, added
9098    /// for the identical reason `CondGotoCounter` above already documents a
9099    /// deviation from this file's own "frozen `SegTerminator`" policy note:
9100    /// that note was written to rule out `ForkJoin` specifically because
9101    /// NOTHING had implemented fork/join scheduling yet; this task is that
9102    /// implementation for `StatementBlockKind::JoinAll` (codegen enforces
9103    /// the JoinAll-only scope with a named `LoweringError` for `JoinAny`/
9104    /// `JoinNone` -- this type carries `kind` through losslessly so codegen
9105    /// has it to check, rather than this crate silently narrowing what it
9106    /// represents). `branches` is exactly [`Terminator::ForkJoin`]'s own
9107    /// field, carried through unchanged (each branch is still a full,
9108    /// independently suspend-capable nested [`ProcessCfg`] -- codegen
9109    /// recursively lowers each one into its own real compiled kernel).
9110    /// `join_blk`/`live_out` mirror `Suspend`'s own `resume`/`live_out`
9111    /// fields exactly: a fork/join site suspends the calling process (until
9112    /// every spawned child completes, for `JoinAll`) just as surely as a
9113    /// `Suspend` terminator does, so the same permanent-frame-slot
9114    /// liveness/writeback discipline applies -- see [`lower_to_segments`]'s
9115    /// own fill rule.
9116    ForkJoin {
9117        kind: StatementBlockKind,
9118        branches: Vec<ProcessCfg<'d>>,
9119        join_blk: SegId,
9120        live_out: Vec<u32>,
9121    },
9122    /// The lowering of [`Terminator::Assertion`].
9123    AssertGoto {
9124        stmt: Statement<'d>,
9125        pass: SegId,
9126        fail: SegId,
9127        done: SegId,
9128    },
9129    /// The lowering of [`Terminator::Case`]: goes to `arms[i]` for the first
9130    /// item of `stmt` whose labels match its selector, else `default`.
9131    CaseGoto {
9132        stmt: Statement<'d>,
9133        arms: Vec<SegId>,
9134        default: SegId,
9135    },
9136}
9137
9138/// PR-041/042/043: a 1:1, source-order-stable (CG-098, CG-099) lowering of
9139/// each `ProcessCfg` [`BasicBlock`] into a [`Segment`] -- `blocks[i]` always
9140/// becomes `SegId(i)`, so segment numbering is fixed the moment `build_cfg`
9141/// allocates its blocks, well before this function (or any LLVM `Context`)
9142/// ever runs.
9143///
9144/// **Task S3b's `live_in`/`live_out` fill rule:** every `FrameLayout` slot
9145/// with source `FrameSlotSource::DeclaredVar` is a *permanent* slot
9146/// (PR-017/018 -- valid for the process's entire lifetime, never
9147/// added/removed by liveness), so its index belongs in `live_in`/`live_out`
9148/// at every segment, unconditionally. A `FrameSlotSource::Temporary` slot's
9149/// index would additionally need to appear only where
9150/// `liveness::compute_liveness`'s own backward fixpoint says it is actually
9151/// live; this task's own restricted CFG-level IR never has a positive source
9152/// for a `Temporary` slot at all (one is only ever minted once CODEGEN's
9153/// Task S3c starts decomposing expressions into named SSA values with no
9154/// corresponding `sv_lang` symbol), so `frame.slots` here is, for every
9155/// fixture this task can construct, exactly the `DeclaredVar` set -- see
9156/// `liveness.rs`'s own module doc comment for the full reasoning. A future
9157/// Task S3c revision of this function additionally intersects
9158/// `compute_liveness`'s per-segment result with whatever `Temporary` slots
9159/// exist by then; this rule does not change for `DeclaredVar` slots.
9160pub fn lower_to_segments<'d>(cfg: &ProcessCfg<'d>, frame: &FrameLayout) -> Vec<Segment<'d>> {
9161    // R-run Task 8: a `repeat` loop's synthetic counter is ALSO permanent --
9162    // it must survive across a suspend/resume call boundary exactly like a
9163    // declared local (see `FrameSlotSource::SyntheticCounter`'s own doc
9164    // comment), so its slot index belongs in `live_in`/`live_out` at every
9165    // segment unconditionally too, same rule as `DeclaredVar`.
9166    let permanent: Vec<u32> = frame
9167        .slots
9168        .iter()
9169        .enumerate()
9170        .filter(|(_, s)| {
9171            matches!(
9172                s.source,
9173                FrameSlotSource::DeclaredVar(_)
9174                    | FrameSlotSource::SyntheticCounter(_)
9175                    | FrameSlotSource::Temporary(_)
9176            )
9177        })
9178        .map(|(i, _)| i as u32)
9179        .collect();
9180
9181    cfg.blocks
9182        .iter()
9183        .enumerate()
9184        .map(|(i, b)| Segment {
9185            id: SegId(i as u32),
9186            live_in: permanent.clone(),
9187            bindings: b.bindings.clone(),
9188            this_var: b.this_var,
9189            ref_aliases: b.ref_aliases.clone(),
9190            body: b.stmts.clone(),
9191            probes: b.probes.clone(),
9192            disable_probes: b.disable_probes.clone(),
9193            frame_probes: b.frame_probes.clone(),
9194            block_events: b.block_events.clone(),
9195            stmt_scopes: b.stmt_scopes.clone(),
9196            step_exprs: b.step_exprs.clone(),
9197            repeat_init: b.repeat_init.clone(),
9198            repeat_decrement: b.repeat_decrement,
9199            expect_resume: b.expect_resume,
9200            terminator: lower_terminator(&b.terminator, &permanent),
9201        })
9202        .collect()
9203}
9204
9205/// Task 1: `true` iff any block in `cfg` closes with `Terminator::ForkJoin`
9206/// -- fork/join has no runtime scheduling semantics this wave (`docs/
9207/// design/process.md` §7: "construction only, never scheduled"), so
9208/// [`lower_to_segments`]/`lower_terminator` cannot honestly produce a
9209/// `SegTerminator` for one (there is no shape in that frozen, codegen-
9210/// exhausted enum for "spawn N branches" -- see this file's module doc
9211/// comment's `SegTerminator`-freeze note). Every real caller (this crate's
9212/// own `sv-sim-bin::run`) must check this and fail loudly *before* calling
9213/// `lower_to_segments` on a CFG that might contain one; `lower_terminator`
9214/// itself still documents and asserts the precondition (`unreachable!`) as
9215/// a caller-discipline internal invariant, exactly like `finish()`'s own
9216/// "every block must be closed" `.expect()` -- not a condition reachable
9217/// from malformed SV source once a caller has checked this predicate first.
9218pub fn contains_unschedulable_fork_join(cfg: &ProcessCfg<'_>) -> bool {
9219    cfg.blocks
9220        .iter()
9221        .any(|b| matches!(b.terminator, Terminator::ForkJoin { .. }))
9222}
9223
9224/// R-run Task 17 (PR-047/PR-061): `true` iff any block in `cfg` closes with
9225/// a `Terminator::ForkJoin` whose `kind` is `JoinAny`/`JoinNone` -- the two
9226/// fork/join shapes this task's own scope note ("Scope for this task:
9227/// `JoinAll` only") leaves unimplemented at the codegen layer.
9228/// **Deliberately narrower than [`contains_unschedulable_fork_join`]
9229/// above, which this function does NOT replace or modify** (that
9230/// function's existing callers -- `sv-sim-bin::aot`, which this task does
9231/// not extend to support ANY fork/join shape -- keep their original,
9232/// unchanged "reject every fork/join" behavior unaffected): a `JoinAll`
9233/// fork/join now has a real [`SegTerminator::ForkJoin`] shape
9234/// [`lower_to_segments`] can produce, so a caller that DOES support it
9235/// (`sv-sim-bin::run`) needs a predicate that only rejects the two kinds
9236/// still genuinely unsupported, letting `JoinAll` flow through to
9237/// `lower_to_segments`/codegen instead of being rejected pre-emptively.
9238pub fn contains_unsupported_fork_join_kind(cfg: &ProcessCfg<'_>) -> bool {
9239    cfg.blocks.iter().any(|b| {
9240        matches!(
9241            b.terminator,
9242            Terminator::ForkJoin {
9243                kind: StatementBlockKind::JoinAny | StatementBlockKind::JoinNone,
9244                ..
9245            }
9246        )
9247    })
9248}
9249
9250fn lower_terminator<'d>(t: &Terminator<'d>, permanent: &[u32]) -> SegTerminator<'d> {
9251    match t {
9252        Terminator::Fallthrough(b) => SegTerminator::Goto(SegId(b.0)),
9253        Terminator::Switch {
9254            on,
9255            arms,
9256            default_blk,
9257            report,
9258        } => SegTerminator::SwitchGoto {
9259            on: on.clone(),
9260            arms: arms.iter().map(|b| SegId(b.0)).collect(),
9261            default: SegId(default_blk.0),
9262            report: *report,
9263        },
9264        Terminator::Branch {
9265            cond,
9266            then_blk,
9267            else_blk,
9268        } => SegTerminator::CondGoto {
9269            cond: *cond,
9270            t: SegId(then_blk.0),
9271            f: SegId(else_blk.0),
9272        },
9273        Terminator::Suspend { point, resume_blk } => SegTerminator::Suspend {
9274            point: point.clone(),
9275            resume: SegId(resume_blk.0),
9276            live_out: permanent.to_vec(),
9277        },
9278        Terminator::End { loop_back } => SegTerminator::End {
9279            loop_back: loop_back.map(|b| SegId(b.0)),
9280        },
9281        // Task 1: `Loop` lowers losslessly to `SegTerminator::Goto` -- see
9282        // the module doc comment's `SegTerminator`-freeze note. Its real
9283        // per-iteration decision already lives on whichever block `entry`
9284        // names (a real `Branch`, or `Fallthrough` for `forever`), so
9285        // nothing this enum could represent is lost.
9286        Terminator::Loop { entry, .. } => SegTerminator::Goto(SegId(entry.0)),
9287        // R-run Task 8: see `SegTerminator::CondGotoCounter`'s own doc
9288        // comment for why this is a real, new lowering (not a lossless
9289        // `Goto`/`CondGoto` reuse the way `Loop` above is).
9290        Terminator::RepeatCheck {
9291            counter,
9292            then_blk,
9293            else_blk,
9294        } => SegTerminator::CondGotoCounter {
9295            counter: *counter,
9296            t: SegId(then_blk.0),
9297            f: SegId(else_blk.0),
9298        },
9299        // R-run Task 17: no longer unreachable -- see `SegTerminator::
9300        // ForkJoin`'s own doc comment. Lossless: `branches`/`kind` carry
9301        // straight through, `join_blk`/`live_out` get the exact same
9302        // treatment `Suspend`'s `resume`/`live_out` get above (a fork/join
9303        // site is a real suspend point too).
9304        Terminator::ForkJoin {
9305            kind,
9306            branches,
9307            join_blk,
9308        } => SegTerminator::ForkJoin {
9309            kind: *kind,
9310            branches: branches.clone(),
9311            join_blk: SegId(join_blk.0),
9312            live_out: permanent.to_vec(),
9313        },
9314        Terminator::Assertion {
9315            stmt,
9316            pass_blk,
9317            fail_blk,
9318            done_blk,
9319        } => SegTerminator::AssertGoto {
9320            stmt: *stmt,
9321            pass: SegId(pass_blk.0),
9322            fail: SegId(fail_blk.0),
9323            done: SegId(done_blk.0),
9324        },
9325        Terminator::Case {
9326            stmt,
9327            arms,
9328            default_blk,
9329        } => SegTerminator::CaseGoto {
9330            stmt: *stmt,
9331            arms: arms.iter().map(|b| SegId(b.0)).collect(),
9332            default: SegId(default_blk.0),
9333        },
9334    }
9335}
9336
9337// --- PR-074: cross-check against AnalyzedProcedure::timing_controls() ---
9338
9339/// Counts every `Timed` statement anywhere in `body`'s subtree, independent
9340/// of whether `build_cfg` would actually accept each one (a rejected
9341/// `TimingControlKind` is still a `Timed` statement) -- this subsystem's own
9342/// "how many suspend points did the tree walk find" half of PR-074's
9343/// cross-check.
9344pub fn count_timed_statements(body: &Statement<'_>) -> usize {
9345    let mut count = 0usize;
9346    let mut stack = vec![body.as_sem_node()];
9347    while let Some(n) = stack.pop() {
9348        if let Some(s) = n.as_statement()
9349            && s.kind() == StatementKind::Timed
9350        {
9351            count += 1;
9352        }
9353        stack.extend(live_children(&n));
9354    }
9355    count
9356}
9357
9358/// PR-074: "the subsystem's own suspend-point tree walk must be cross-checked
9359/// against `AnalyzedProcedure::timing_controls()`; a mismatch ... MUST be a
9360/// hard-fail bug, not a warning." Factored as a pure comparator over two
9361/// counts (rather than requiring every caller to thread a live
9362/// `sv_lang::Analysis` through) so a test can fault-inject a mismatch
9363/// directly -- see `cross_check_hard_fails_on_a_fault_injected_mismatch` in
9364/// `tests/process_cfg.rs`. The real call site passes
9365/// `own_walk_count = count_timed_statements(body)` and
9366/// `analyzed_count = analyzed_procedure.timing_controls().count()`.
9367pub fn check_suspend_count_matches(
9368    own_walk_count: usize,
9369    analyzed_count: usize,
9370) -> Result<(), LoweringError> {
9371    if own_walk_count == analyzed_count {
9372        Ok(())
9373    } else {
9374        Err(LoweringError::malformed(format!(
9375            "PR-074 suspend-point cross-check failed: this subsystem's own tree walk found \
9376             {own_walk_count} Timed statement(s), but AnalyzedProcedure::timing_controls() \
9377             found {analyzed_count} -- this is a hard bug (a missed or double-counted \
9378             suspend point), never a warning"
9379        )))
9380    }
9381}