Skip to main content

vj_sim/process/
cfg.rs

1//! Task S3a (`docs/superpowers/plans/2026-09-26-wave-A2-slice.md`): the CPS
2//! control-flow-graph builder and its Segment IR lowering, restricted to
3//! exactly the A2-slice's two suspend kinds (`Delay`, `Edge` -- i.e.
4//! `SignalEventControl` with `posedge`/`negedge`). Ledger PR-004, PR-005,
5//! PR-006 (Delay/SignalEvent half only), PR-007, PR-008, PR-041, PR-042,
6//! PR-043, PR-044, PR-048, PR-069, PR-074, PR-114, CG-098, CG-099. See
7//! `docs/design/process.md` §§1-6.
8//!
9//! **Task 1 (`docs/superpowers/plans/2026-09-26-wave-A2-process-kernels.md`,
10//! ledger PR-006/PR-045) extends the above** with the rest of the
11//! suspend-relevant `StatementKind` taxonomy: `Wait` (level-sensitive,
12//! `SuspendPoint::Level` -- CFG shape only, the "sometimes there's no
13//! suspend at all" re-check behavior is Task 3's), `EventTrigger` (split
14//! only when it carries a `DelayControl` timing; an undelayed `->`/`->>`
15//! never suspends), a fork/join `Block` (`Terminator::ForkJoin`,
16//! construction only -- never scheduled, see `docs/design/process.md` §7),
17//! and `Terminator::Loop{header,body,exit,entry}` for `for`/`while`/
18//! `do-while`/`forever`/`foreach` (PR-045). `WaitFork`/`WaitOrder`/
19//! `DisableFork` remain named [`LoweringError`]s (recognized, never silently
20//! ignored, but not yet schedulable -- fork/join has no runtime semantics
21//! this wave either way). Two more `TimingControlKind`s get real handling:
22//! `EventList` (as `SuspendPoint::EdgeList`) and `CycleDelay` (lowered like
23//! `Delay` against a single inferred clock, matching `docs/design/
24//! process.md` §2's own explicitly-flagged stub scope).
25//! `ImplicitEvent`/`RepeatedEvent`/`Delay3`/`OneStepDelay`/`BlockEventList`
26//! remain named `LoweringError`s: `docs/design/process.md` §2's own
27//! `SuspendPoint` enum (the binding design record) has exactly six variants
28//! (`Delay`/`Edge`/`EdgeList`/`ImplicitComb`/`Level`/`CycleDelay`) and does
29//! not admit these five as first-class suspend shapes at all, and this
30//! file's own extension policy (below) forbids adding a placeholder variant
31//! ahead of a real implementation -- so PR-006 stays `"partial"` in
32//! `docs/ledger.json` after this task, not `"done"` (see this task's own
33//! deviations note for the full accounting).
34//!
35//! **Task 6 (`docs/superpowers/plans/2026-09-27-run-wiring.md`, ledger
36//! CG-081/CG-171) closes most of the above gap.** `SuspendPoint` gains real
37//! variants for `ImplicitEvent` (`ImplicitComb`) and `OneStepDelay`
38//! (`OneStep`), and `CycleDelay` is revised to carry its inferred clock
39//! signal/edge as real payload (see that variant's own doc comment for the
40//! full CG-171 reasoning, including a recorded deviation from the plan's
41//! literal `analysis::ClockDomainId`-carrying wire design). Two kinds stay
42//! named `LoweringError`s after this task too, each for a DIFFERENT,
43//! verified reason -- neither is "no `SuspendPoint` variant exists for it"
44//! any more: `BlockEventList` is only a covergroup sampling event (LRM 19.3;
45//! slang rejects it as a procedural timing control at parse, see the
46//! `BlockEventList` arm in `walk_timed`); `RepeatedEvent` is structurally UNREACHABLE at this dispatch
47//! point at all -- verified empirically (a throwaway AST-dumping test) that
48//! `RepeatedEventControl` is never the outer `TimingControl` of a
49//! `StatementKind::Timed` node, only ever an intra-assignment timing control
50//! embedded in an `Assignment` expression's own children, a structurally
51//! different position needing a `Temporary`-frame-slot CFG-splitting
52//! mechanism this AST-only layer does not have -- see the `RepeatedEvent`
53//! arm in `walk_timed` for the full citation and this task's deviations for
54//! the full accounting. `Delay3` remains rejected unconditionally too, but
55//! because it is semantically inapplicable to a procedural suspend point at
56//! all (gate-primitive delay syntax), not a gap.
57//!
58//! **Task 3 (`docs/superpowers/plans/2026-09-26-wave-A2-process-kernels.md`
59//! "A2-part2", ledger CG-169/CG-170/PR-106/CG-115) gives `Wait`/`SignalEvent`
60//! their real, previously-deferred semantics.** [`Self::walk_wait`] no
61//! longer closes `cur` with an unconditional `Terminator::Suspend` -- see its
62//! own doc comment for the `Terminator::Branch`-based "immediate check,
63//! conditional suspend" shape LRM 9.4.3/CG-169 requires. `SuspendPoint::Edge`
64//! gains an `iff: Option<Expression<'d>>` field (CG-170,
65//! `SignalEventControl.iffCondition`), extracted positionally from
66//! `TimingControl::children()` by [`extract_iff`] -- see that function's own
67//! doc comment for the verified-live `sv-lang`=0.1.9 binding shape this
68//! relies on. Neither change touches `SuspendPoint::EdgeList`: Task 1's own
69//! doc comment on that variant already scopes `iff` extraction to the
70//! narrower `(Expression, EdgeKind)` pair with no `iff`, and this task does
71//! not revisit that decision.
72//!
73//! **`disable label;` (`StatementKind::Disable`, distinct from
74//! `DisableFork`).** The pinned `sv-lang`=0.1.9 binding exposes no way to
75//! read `DisableStatement::target`: the C API has no `slang_stmt_disable_*`
76//! accessor, and the generic `children()`/`expressions()` walk never
77//! reaches `target` (`DisableStatement` defines no `visitExprs`) -- a
78//! `disable label;` statement's own `children().len()` is `0`. The label is
79//! therefore read from the statement's source span in the design's source
80//! text (registered by the caller, [`set_source_text`]), accepted only when
81//! the span reads exactly `disable <identifier> ;` (see `disable_label`).
82//! A named sequential block containing a `disable` gets its own exit block
83//! (`disable_targets`), and `disable blk;` inside it is a jump there, like
84//! `break` (LRM 1800-2023 §9.6.2). In a branch of the named fork block `f`,
85//! `disable f;` stays a statement (codegen: `sv_rt_disable_fork_block`,
86//! ending every branch of the fork) followed by the branch's end. Any other
87//! target -- another process's block, a task, an outer fork, an unreadable
88//! span -- is a named `LoweringError`.
89//!
90//! **Algorithm (PR-007):** [`build_cfg`] is a single recursive-descent walk
91//! of a process body's `Statement` tree (`children()`/`statements()`/
92//! `then_branch()`/`else_branch()`/`body()`/`condition()`/`expr()`) -- an
93//! ordinary CFG builder -- with the ONE addition that entering a `Timed`
94//! node wrapping a `Delay` or `SignalEvent` (posedge/negedge only) timing
95//! control closes the current [`BasicBlock`] with `Terminator::Suspend` and
96//! opens a fresh block for the guarded statement that follows. Every other
97//! suspend-relevant `StatementKind` (`Wait`, `WaitFork`, `WaitOrder`,
98//! `EventTrigger`, `DisableFork`, a fork/join `Block`) and every other
99//! `TimingControlKind` a `Timed` node could wrap (`EventList`,
100//! `ImplicitEvent`, `RepeatedEvent`, `Delay3`, `OneStepDelay`, `CycleDelay`,
101//! `BlockEventList`) is a named [`LoweringError`], never silently treated as
102//! ordinary control flow (PR-006's slice-scope half) -- Task 1 above lifts
103//! this restriction for `Wait`/`EventTrigger`/fork-join `Block`/`EventList`/
104//! `CycleDelay` specifically.
105//!
106//! **Extension policy (PR-069):** [`Terminator`] and [`SuspendPoint`]
107//! intentionally have exactly the variants a task has actually implemented
108//! -- no placeholder variant is ever added ahead of a real implementation,
109//! because an unused variant would force every `match` in this file (and
110//! every future consumer's) to either wildcard it (forbidden by ARCHITECTURE
111//! §7 item 9) or handle a case that can never actually be constructed (a
112//! "gate that cannot fail", forbidden by this wave's correctness posture).
113//! Instead: a task adding a new suspend kind or terminator shape adds the
114//! variant *and* is required to re-audit every `match` site in this file,
115//! since none has a wildcard arm to silently swallow it -- Task 1 did
116//! exactly this for `Loop`/`ForkJoin`/`EdgeList`/`Level`/`CycleDelay` (but
117//! deliberately NOT `Disable` -- see above). Until a shape has a real
118//! implementation, every code path that *would* need it returns a named
119//! [`LoweringError`] instead. `SegTerminator` (PR-041/042/043,
120//! `lower_terminator` below) deliberately keeps its *own* variant set frozen
121//! at `Goto`/`CondGoto`/`Suspend`/`End` -- it is exhaustively matched by
122//! `vj-codegen` (a different track's contract) with no wildcard arm, so
123//! `Terminator::Loop` lowers losslessly to `SegTerminator::Goto` (a real,
124//! already-populated successor block -- `entry` -- always carries the
125//! actual decision, whether that is `header`'s own `Branch` or a plain
126//! `Fallthrough`) rather than adding a new `SegTerminator` variant that
127//! would break that exhaustive match. A `Terminator::ForkJoin` reaching
128//! `lower_terminator` is a caller-discipline precondition violation (see
129//! [`contains_unschedulable_fork_join`]), not a shape `SegTerminator` can
130//! represent at all this wave -- fork/join has no runtime scheduling
131//! semantics yet, so no `SegTerminator` value would be honest here; every
132//! real caller must check that predicate first.
133use crate::symid::SymbolUid;
134use std::collections::HashSet;
135use std::fmt;
136
137use sv_lang::kinds::{ExpressionKind, StatementKind, SymbolKind, TimingControlKind};
138use sv_lang::{
139    EdgeKind, Expression, KnownSystemName, SemNode, Statement, StatementBlockKind, Symbol,
140    TimingControl, UniquePriorityCheck,
141};
142
143/// Every unhandled AST shape this file's builders encounter produces one of
144/// these -- never `panic!`/`unimplemented!`/a silent no-op. Deliberately a
145/// separate type from `vj-codegen::error::LoweringError`: this crate must
146/// never depend on `vj-codegen` (the dependency edge runs the other way
147/// once Task S3c lands), so it cannot reuse that one.
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct LoweringError {
150    pub message: String,
151}
152
153impl LoweringError {
154    // `pub(crate)` (Task S3b deviation, not `fn`-private as Task S3a left
155    // them): `process::liveness` is a sibling module that needs to construct
156    // the same error shape (declared-local width/shape rejections, malformed
157    // block-scope walks) rather than inventing a second `LoweringError`-like
158    // type just for one module boundary. Still crate-private -- no outside
159    // crate constructs one of these directly, only ever sees it through
160    // `Result::Err`.
161    pub(crate) fn unsupported(what: impl Into<String>) -> Self {
162        Self {
163            message: format!("unsupported: {}", what.into()),
164        }
165    }
166
167    pub(crate) fn malformed(what: impl Into<String>) -> Self {
168        Self {
169            message: format!("malformed AST: {}", what.into()),
170        }
171    }
172
173    /// PR-022's hard, typed self-check failure: `process::liveness::
174    /// compute_liveness`'s backward fixpoint computed a live-in symbol that
175    /// `AnalyzedProcedure::read_set()` (or, absent a live `Analysis` handle,
176    /// this crate's own AST-derived read-set aggregate -- see that module's
177    /// doc comment) never reports as read anywhere in the whole procedure.
178    /// Kept as a named constructor on this same flat `LoweringError` shape
179    /// (deviation from the plan's literal `LoweringError::
180    /// LivenessOracleMismatch` enum-variant phrasing: Task S3a already
181    /// committed `LoweringError` as a message-carrying struct with named
182    /// constructors, not an enum, so this follows that established pattern
183    /// rather than introducing a second, incompatible error type mid-crate)
184    /// -- the message names the offending symbol, so a test asserts on
185    /// content, not just `is_err()`, per this file's own established
186    /// discipline.
187    pub(crate) fn liveness_oracle_mismatch(symbol_name: impl Into<String>) -> Self {
188        Self {
189            message: format!(
190                "LivenessOracleMismatch: computed live-in symbol {:?} is absent from the \
191                 whole-procedure read_set -- this is a hard bug in the liveness walk, never \
192                 a legitimate program (PR-022)",
193                symbol_name.into()
194            ),
195        }
196    }
197}
198
199impl fmt::Display for LoweringError {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        write!(f, "{}", self.message)
202    }
203}
204
205impl std::error::Error for LoweringError {}
206
207/// Dense, per-process `ProcessCfg::blocks` index, assigned in AST pre-order
208/// (first-appearance-in-source) allocation order (CG-098, CG-099, PR-114) --
209/// completely independent of any later LLVM basic-block layout order, since
210/// no LLVM `Context`/`Module` exists yet when `build_cfg` runs.
211#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
212pub struct BlockId(pub u32);
213
214/// Dense `Segment` index -- see [`BlockId`]'s doc comment; `lower_to_segments`
215/// assigns `SegId(i)` to the block at index `i`, a 1:1, order-preserving
216/// mapping (PR-041..PR-043).
217#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
218pub struct SegId(pub u32);
219
220/// R-run Task 8 (PR-045/PR-006): a `repeat(N) body;` loop's compiler-
221/// synthesized iteration counter, minted once per `RepeatLoop` statement
222/// [`CfgBuilder::walk_repeat_loop`] decomposes into a real CFG shape (i.e.
223/// only when `body` contains a suspend point somewhere in its own subtree --
224/// the suspend-free case stays the pre-existing opaque leaf `stmt_shared::
225/// lower_repeat_loop` already handles unchanged, see `walk_stmt`'s own
226/// dispatch comment). Unlike a `for` loop's user-visible control variable
227/// (a real `sv_lang::Symbol`, `FrameSlotSource::DeclaredVar`), `repeat`'s
228/// count has no corresponding declared local at all -- there is nothing a
229/// `Branch`'s `cond: Expression` could re-evaluate every iteration the way
230/// `while`/`for` do. This id is this crate's own, `sv_lang`-free handle for
231/// that counter's eventual `FrameSlotSource::SyntheticCounter` frame slot
232/// (`process::liveness`) -- dense per-`ProcessCfg`, assigned in allocation
233/// order (mirrors [`BlockId`]/[`SegId`]'s own numbering discipline), never
234/// reused across two distinct `repeat` statements in the same process (so a
235/// nested or sibling `repeat` gets its own, independent counter slot).
236#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
237pub struct RepeatCounterId(pub u32);
238
239/// The suspend kinds this file implements. `Delay`/`Edge` are PR-006's
240/// slice-scope half (Task S3a); `EdgeList`/`Level`/`CycleDelay` are Task 1's
241/// addition (PR-006's Part 2 half). `ImplicitComb` remains unimplemented --
242/// see the module doc comment's PR-006 accounting.
243#[derive(Debug, Clone)]
244pub enum SuspendPoint<'d> {
245    /// `#<delay>`. The second field is the inlined subroutine whose body holds the delay, when
246    /// one does: a delay's time unit is the unit of the scope that DECLARES it (LRM 3.14.2.1),
247    /// not the caller's.
248    Delay(Expression<'d>, Option<Symbol<'d>>),
249    Edge {
250        signal: Expression<'d>,
251        edge: EdgeKind,
252        /// `SignalEventControl.iffCondition` (LRM `@(posedge clk iff en)`,
253        /// Task 3/CG-170): `None` when no `iff` qualifier is present. The
254        /// kernel's resume prologue for a suspend point carrying `Some(_)`
255        /// ALWAYS re-evaluates it on wake and, if false, immediately
256        /// re-returns `Suspended` with the identical `WaitSpec` unchanged --
257        /// it does not advance the PC and does not re-run the segment body
258        /// (`docs/design/process.md` §2's own "iff re-check" paragraph;
259        /// `compile_iff_guard`'s doc comment in `vj-codegen::
260        /// process_lower` is the codegen half of this same contract).
261        iff: Option<Expression<'d>>,
262    },
263    /// `EventListControl` (`@(a or b or c)`): wake on any member. Deviation
264    /// from `docs/design/process.md` §2's literal `Vec<{edge,signal,iff}>`
265    /// triple -- Task 1's own "Public names/types introduced" text specifies
266    /// the narrower `(Expression, EdgeKind)` pair with no `iff` (matching
267    /// how `Level`'s own `iff`/re-check contract is deferred to Task 3);
268    /// this is the more specific, more recent spec and is followed exactly.
269    ///
270    /// The second field holds each member's `iff` guard (LRM 1800-2023
271    /// 9.4.2.3: `@(posedge a iff c or b)`): always the same length as the
272    /// member list, empty for an unguarded member. The list wakes on a
273    /// member's event only if every one of that member's guards holds then
274    /// (more than one when an `iff` meets an `$inferred_clock` whose own
275    /// clocking event has an `iff`, a conjunction: LRM 17.4, 9.4.2.3).
276    EdgeList(Vec<(Expression<'d>, EdgeKind)>, Vec<Vec<Expression<'d>>>),
277    /// `wait(expr)` (LRM 9.4.3, level-sensitive). Task 3: [`CfgBuilder::
278    /// walk_wait`] wires this as the `else_blk` of a `Terminator::Branch` on
279    /// `expr` itself (never unconditionally) -- "evaluate immediately,
280    /// suspend only if false" (`docs/design/process.md` §2's own "wait(expr)
281    /// special case", CG-169). The per-wake re-check (re-suspending
282    /// unchanged on a spurious wake, CG-115) is entirely codegen's job
283    /// (`vj-codegen::process_lower::compile_iff_guard`'s resume
284    /// prologue) -- this crate never evaluates `expr`.
285    Level(Expression<'d>),
286    /// `##N` (clocking-block-relative cycle delay) -- **Task 6 (CG-171)
287    /// revision:** no longer a bare cycle count. ARCHITECTURE §6.5 represents
288    /// `CycleDelayControl` as "kept as an unresolved expression + ClockDomain
289    /// for the scheduler to convert cycles->ticks at runtime", i.e. codegen
290    /// must never precompute an absolute tick offset (a clock's period can
291    /// change dynamically -- see `sched::mod::EventTerm::Repeat`'s own SC-053
292    /// doc comment for the differentiating behavior this rules out). This
293    /// variant carries `count` (the compile-time-constant cycle count --
294    /// still restricted the same way `##N` always was, see
295    /// [`CfgBuilder::walk_timed`]'s `CycleDelay` arm) plus the syntactically
296    /// inferred single clock this process suspends against, `clock`/
297    /// `edge` (or, with a default clocking block in scope, that block
298    /// itself with `BothEdges`: `##N` counts its events) -- the SAME `inferred_clock` this file already tracked before
299    /// this task (previously computed then silently discarded once
300    /// ambiguity-checked; now threaded through as real payload). Resolving
301    /// `clock` to a concrete `StateCellId` still happens at codegen
302    /// time (`vj-codegen::process_lower::resolve_suspend_point`, exactly
303    /// like `Edge`'s own signal) -- what does NOT happen at codegen time is
304    /// converting `count` cycles into an absolute tick number: that stays a
305    /// live, per-edge `EventTerm::Repeat{count, inner: Edge{cell, edge}}`
306    /// wait the SCHEDULER counts down at runtime (already built and tested,
307    /// SC-052/SC-053), so a clock whose period later changes still produces
308    /// the correct absolute timing by construction, with no cached tick
309    /// offset to go stale.
310    ///
311    /// **Deviation from the plan's literal `analysis::ClockDomainId`-carrying
312    /// wire design, and a real, verified SCOPE LIMIT (both recorded in full
313    /// in this task's deviations -- not silently narrowed):** the plan's own
314    /// text describes resolving `clock` against a live `sv-lang`-
315    /// `Analysis`-backed WOCB partition result (`analysis::
316    /// StaticAnalysisResult::clock_domains()`) threaded all the way from
317    /// `vj::run`'s elaboration pipeline through to the scheduler's
318    /// `Engine`. That plumbing does not exist anywhere in this tree yet
319    /// (`analysis::analyze()` is never called from `run.rs`/`aot.rs`/
320    /// `pipeline.rs` today), and building it is a genuinely separate,
321    /// `run.rs`-touching undertaking this task's own binding rules forbid
322    /// risking (wave-B-runtime is concurrently landing in that exact file).
323    /// This variant instead reuses the clock inference this crate ALREADY
324    /// performed for every `##N` before this task (the same
325    /// `inferred_clock` ambiguity check, now threaded through as real
326    /// payload instead of discarded) -- **which is genuinely narrower than
327    /// "the process's own inferred clock domain," not merely a smaller-but-
328    /// equivalent instance of it, and this narrowing is load-bearing, not
329    /// cosmetic:** it resolves `##N` written inside a process that ALSO
330    /// itself syntactically sights a `posedge`/`negedge` edge somewhere
331    /// earlier (the common `always_ff @(posedge clk) ... ##N ...` idiom),
332    /// but does NOT resolve a `##N` relative to a `default clocking`/named
333    /// `clocking cb @(posedge clk); endclocking` block referenced from a
334    /// process with NO local edge sighting of its own -- verified directly
335    /// (a throwaway test compiling ARCHITECTURE §6.5's own canonical
336    /// clocking-block fixture, `default clocking cb; ... initial ##3 x=1;`,
337    /// with no `@(posedge clk)` anywhere in that second `initial` block):
338    /// `walk_timed`'s `CycleDelay` arm returns the SAME "no unambiguous
339    /// single inferred clock in scope" `LoweringError` for it that a
340    /// genuinely unclocked `##N` gets, because `default clocking`/
341    /// `ClockingBlock` declarations are never read by this mechanism at
342    /// all. Reading them is also a `session.rs`-owned, pre-existing,
343    /// verified-separately-blocked gap regardless (`vj-codegen::
344    /// session.rs`'s own structural-symbol classification already places
345    /// `SymbolKind::ClockingBlock` in its "no correct lowering exists yet"
346    /// bucket -- a design containing one fails upstream of this crate
347    /// entirely), so closing it is not a smaller extension of this task's
348    /// own mechanism, it needs a different, cross-track-blocked one. CG-171
349    /// therefore closes for real, common `##N` usage (a clock edge
350    /// syntactically visible in the SAME process) but stays open for the
351    /// `default clocking`-bound shape -- see this task's deviations for the
352    /// full accounting, and `crates/vj-harness/tests/
353    /// slice_vs_iverilog.rs::clocking_cycle_delay_matches_iverilog_bit_for_bit`
354    /// for the still-`#[ignore]`d e2e proof (updated by this task to name
355    /// this exact, narrower residual, not the stale "CG-171 unimplemented"
356    /// reason it carried before). See `sched::reschedule_req_proposal::
357    /// KIND_REPEAT_EDGE`'s own doc comment for the wire shape the resolved
358    /// (local-edge) case resolves into -- a single-cell, single-edge repeat
359    /// wait that fits the frozen 24-byte budget inline (no out-of-line
360    /// table needed, unlike `EdgeList`/`ImplicitComb`).
361    ///
362    /// **Verified, separately-recorded finding (not this task's bug to fix,
363    /// but load-bearing for why no NEW oracle-diffed e2e fixture proves this
364    /// closure end-to-end):** `##N` outside a `default clocking` context is
365    /// not accepted as valid SystemVerilog by either reference tool this
366    /// crate uses -- `iverilog -g2012` rejects a bare `##2` inside an
367    /// ordinary `always_ff` body with a flat parser "syntax error", and
368    /// `verilator --binary -sv` rejects the identical construct with
369    /// "Usage of cycle delays requires default clocking (IEEE 1800-2023
370    /// 14.11)" (both verified directly, by hand, against a throwaway
371    /// fixture). This mechanism's "syntactically inferred single clock"
372    /// acceptance criterion PRE-DATES this task (an earlier task's own
373    /// `inferred_clock` field, which this task only threads through as real
374    /// payload instead of discarding) -- this task neither introduces nor
375    /// revisits that acceptance decision, only documents, empirically, that
376    /// no available reference oracle can differentially validate it end to
377    /// end: the codegen-level JIT+run round trip (`crates/vj-codegen/
378    /// tests/frame_roundtrip_matrix.rs`) is therefore the strongest
379    /// available proof of this specific closure, not an iverilog/Verilator
380    /// diff.
381    CycleDelay {
382        count: u32,
383        /// The clock: a signal (the process's inferred clock, counted on
384        /// `edge`), or the default clocking block (its event counter,
385        /// counted on any change).
386        clock: Symbol<'d>,
387        edge: EdgeKind,
388        /// A clocking drive THROUGH a virtual interface (`v.sb.c <= ##1 1`,
389        /// LRM 25.9.1): the clock variable's member access. `clock` is then
390        /// the template clocking block, which has no signal; the run time
391        /// counts the events of the clocking block of the instance `v` holds.
392        via: Option<Expression<'d>>,
393    },
394    /// **Task 6 (CG-081):** `ImplicitEventControl` (`@*`/`@(*)`). Fieldless
395    /// per `docs/design/process.md` §2's own `SuspendPoint` sketch would
396    /// suggest (`WaitSpec::ImplicitComb(SubscriptionSetId)` is resolved
397    /// against a separate table, not embedded read-set data) -- deviates by
398    /// carrying the guarded body's own AST-derived read-set directly
399    /// (`Vec<Expression<'d>>`, computed by [`implicit_comb_read_set`]) rather
400    /// than being truly fieldless, because this crate's `build_cfg` has no
401    /// live `sv_lang::Analysis`/`AnalyzedProcedure` handle to later re-derive
402    /// it from at codegen time (see [`implicit_comb_read_set`]'s own doc
403    /// comment for the full reasoning) -- the read-set has to be captured
404    /// HERE, while the guarded body is in hand, or nowhere in this crate.
405    ImplicitComb(Vec<Expression<'d>>),
406    /// **Task 6 (CG-081):** `OneStepDelayControl` (the LRM's built-in
407    /// `1step` delay, one global-precision tick, SC-123). Fieldless -- the AST node itself carries no
408    /// expression (confirmed against the vendored slang C++:
409    /// `OneStepDelayControl` declares no field and no `visitExprs` override
410    /// at all, so `TimingControl::children()` is always empty for this
411    /// kind). Resolved at codegen time to `Suspend::Delay(1)` (LRM 3.14.3: a
412    /// step is the global time precision) -- see
413    /// `vj-codegen::process_lower::resolve_suspend_point`'s `OneStep`
414    /// arm.
415    OneStep,
416    /// `wait fork;` (LRM 9.6.1, SC-125): resume once every child this process
417    /// forked has finished. Payload-free -- the scheduler tracks the children.
418    WaitFork,
419    /// Classes plan Task 13 (LRM 9.7): `p.await();` / `p.suspend();` as a
420    /// statement. The call itself queues a process request; this suspend
421    /// yields so the scheduler applies it before the next statement runs
422    /// (`Suspend::ByRequest`). Payload-free.
423    ByRequest,
424}
425
426/// Task 7 (PR-116, XC-044): a fieldless, `'static`-safe mirror of
427/// [`SuspendPoint`]'s discriminants. `SuspendPoint<'d>` itself cannot be
428/// named `&'static [SuspendPoint]` -- every payload-carrying variant embeds
429/// a borrowed `sv_lang::Expression<'d>`, which is only ever alive for the
430/// lifetime of a real, already-compiled `Design`, so no `'static` instance
431/// of it can exist independent of one. This type is exactly the
432/// "which shape" tag [`suspending_statement_kinds`]'s two callers (the
433/// frame-slot round-trip matrix in `vj-sim`'s own tests and the JIT+run
434/// matrix in `vj-codegen`'s tests) both need, without requiring either
435/// to fabricate a borrowed `Expression` from nothing.
436#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
437pub enum SuspendPointKind {
438    Delay,
439    Edge,
440    EdgeList,
441    Level,
442    CycleDelay,
443    /// Task 6 (CG-081): `ImplicitEventControl` (`@*`/`@(*)`).
444    ImplicitComb,
445    /// Task 6 (CG-081): `OneStepDelayControl` (`#0`).
446    OneStep,
447    /// `wait fork;` (SC-125).
448    WaitFork,
449    /// `process::await()` / `process::suspend()` (Classes plan Task 13).
450    ByRequest,
451}
452
453impl<'d> SuspendPoint<'d> {
454    /// Exhaustive, no-wildcard match (ARCHITECTURE §7 item 9): if a future
455    /// task adds a new `SuspendPoint` variant without extending
456    /// [`SuspendPointKind`] and this match, the crate fails to COMPILE, not
457    /// merely under-tests -- a stronger guarantee than
458    /// [`suspending_statement_kinds`]'s own length alone could give.
459    pub fn kind(&self) -> SuspendPointKind {
460        match self {
461            SuspendPoint::Delay(..) => SuspendPointKind::Delay,
462            SuspendPoint::Edge { .. } => SuspendPointKind::Edge,
463            SuspendPoint::EdgeList(..) => SuspendPointKind::EdgeList,
464            SuspendPoint::Level(_) => SuspendPointKind::Level,
465            SuspendPoint::CycleDelay { .. } => SuspendPointKind::CycleDelay,
466            SuspendPoint::ImplicitComb(_) => SuspendPointKind::ImplicitComb,
467            SuspendPoint::OneStep => SuspendPointKind::OneStep,
468            SuspendPoint::WaitFork => SuspendPointKind::WaitFork,
469            SuspendPoint::ByRequest => SuspendPointKind::ByRequest,
470        }
471    }
472}
473
474/// Task 7 (PR-116, XC-044): every `SuspendPoint` kind this file's CFG
475/// builder currently gives a real shape to (i.e. every variant
476/// [`SuspendPoint`] itself has -- see that enum's own doc comment for the
477/// still-open `ImplicitEvent`/`RepeatedEvent`/`Delay3`/`OneStepDelay`/
478/// `BlockEventList` gap PR-069's extension policy keeps out of the enum
479/// entirely). One definition, promoted from a test-only helper to real
480/// production code specifically so both `vj-sim`'s own meta-test and
481/// `vj-codegen`'s JIT+run matrix enumerate the identical list, never two
482/// independently-maintained copies that could silently drift apart (this
483/// project's own "duplicate pure-function eval diverges" lesson).
484///
485/// **Not every kind in this list is yet resolvable by
486/// `vj-codegen::process_lower::resolve_suspend_point`** --
487/// `EdgeList`/`CycleDelay` are real CFG shapes (this file's own Task 1) but
488/// still a named, codegen-side `LoweringError` ("this SuspendPoint kind has
489/// no codegen-side resolution yet"), a pre-existing, documented scope
490/// boundary from that same task, not something this row's own file list
491/// (`crates/vj-sim/tests/frame_roundtrip_matrix.rs`, `crates/
492/// vj-codegen/tests/frame_roundtrip_matrix.rs`, this file) can close
493/// without expanding into `vj-codegen::process_lower`'s own `Files`
494/// scope. `vj-codegen`'s own round-trip matrix asserts the SPECIFIC,
495/// named rejection for these two kinds (never silently skipping them), so
496/// the gate still fails loudly in both directions: if a kind that should
497/// round-trip stops doing so, or if a kind that should still be rejected
498/// silently starts "succeeding" with an unreviewed codegen change.
499pub fn suspending_statement_kinds() -> &'static [SuspendPointKind] {
500    &[
501        SuspendPointKind::Delay,
502        SuspendPointKind::Edge,
503        SuspendPointKind::EdgeList,
504        SuspendPointKind::Level,
505        SuspendPointKind::CycleDelay,
506        SuspendPointKind::ImplicitComb,
507        SuspendPointKind::OneStep,
508        SuspendPointKind::WaitFork,
509        SuspendPointKind::ByRequest,
510    ]
511}
512
513/// A `BasicBlock`'s single exit (PR-008, extended by Task 1).
514#[derive(Debug, Clone)]
515pub enum Terminator<'d> {
516    /// Falls straight into `BlockId` with no branch/suspend (e.g. the merge
517    /// point after an `if`/`else`).
518    Fallthrough(BlockId),
519    /// Goes to `arms[i]` when `on` evaluates to `i`, else to `default_blk`
520    /// -- reporting an `$error` at source offset `report` first, when set
521    /// (a `wait_order` with no `else`).
522    Switch {
523        on: SwitchOn<'d>,
524        arms: Vec<BlockId>,
525        default_blk: BlockId,
526        report: Option<sv_lang::SourceLoc>,
527    },
528    /// `if`/`case` (PR-044 -- expanded to a br-chain at the `Terminator`
529    /// level; the ternary operator `?:` never appears here, only inside a
530    /// straight-line expression within a [`BasicBlock`]'s `stmts`).
531    Branch {
532        cond: Expression<'d>,
533        then_blk: BlockId,
534        else_blk: BlockId,
535    },
536    /// An immediate assertion (LRM 16.3) whose pass or fail statement waits,
537    /// so the statement cannot stay a straight-line leaf. Goes to `pass_blk`
538    /// (`fail_blk`) when the expression is true (not true) and the assertion
539    /// is checking with that action on (20.11), else to `done_blk`; the
540    /// default `$error` of an assert/assume with no fail statement is
541    /// reported inline before `done_blk`.
542    Assertion {
543        stmt: Statement<'d>,
544        pass_blk: BlockId,
545        fail_blk: BlockId,
546        done_blk: BlockId,
547    },
548    Suspend {
549        point: SuspendPoint<'d>,
550        resume_blk: BlockId,
551    },
552    /// PR-048: `loop_back` is `None` for `initial`/`final` procedures and
553    /// `Some(entry)` for `always`/`always_ff`/`always_comb`/`always_latch`
554    /// procedures -- see [`ProcessLoopKind`].
555    End { loop_back: Option<BlockId> },
556    /// PR-045 (Task 1): `for`/`while`/`do-while`/`forever`/`foreach`.
557    /// `header`'s own terminator (a plain `Branch` for a real per-iteration
558    /// condition, or `Fallthrough` when there is none -- `forever`) carries
559    /// the actual per-iteration decision, and `body`'s own terminator closes
560    /// the back edge to `header`. `entry` is the block `cur` (the one this
561    /// `Terminator::Loop` closes) actually transfers control to on first
562    /// entry -- `header` for `while`/`for`/`forever`/`foreach` (the
563    /// condition, if any, is checked before the first iteration), but
564    /// `body` for `do-while` (the body always runs once before any check) --
565    /// a deviation from `docs/design/process.md` §2's literal 3-field
566    /// `Loop{header,body,exit}` (recorded in this task's deviations): a
567    /// `do-while`'s real entry edge genuinely cannot be recovered from
568    /// `header`/`body`/`exit` alone once `header` is required (as this
569    /// task's own tests require) to remain a distinct, inspectable
570    /// post-body recheck block rather than being aliased to `body`. Codegen
571    /// may fully unroll a static-bound `for`, but the CFG representation
572    /// itself always has a real `Loop` terminator so the liveness/segment-
573    /// lowering passes never special-case "was this unrolled" (this file's
574    /// own `lower_terminator` maps it losslessly to
575    /// `SegTerminator::Goto(entry)`, since whichever real block `entry`
576    /// names already carries its own correct terminator). `foreach`'s
577    /// per-dimension bound is a `ConstantRange`, not an `Expression` slang
578    /// exposes anywhere on the statement -- see `walk_foreach_loop`'s own
579    /// doc comment for why that one kind's `body` is a chain of unrolled
580    /// copies rather than a real back edge (a second, separately-recorded
581    /// deviation).
582    Loop {
583        header: BlockId,
584        body: BlockId,
585        exit: BlockId,
586        entry: BlockId,
587    },
588    /// `fork ... join`/`join_any`/`join_none` (Task 1, PR-069/docs/design/
589    /// process.md §7): construction only, never scheduled. Each
590    /// `branches[i]` is a full nested [`ProcessCfg`] (a fork branch can
591    /// contain its own suspends). See [`contains_unschedulable_fork_join`]
592    /// -- a real caller must check this before calling [`lower_to_segments`]
593    /// on a CFG that might contain one.
594    ForkJoin {
595        kind: StatementBlockKind,
596        branches: Vec<ProcessCfg<'d>>,
597        join_blk: BlockId,
598    },
599    /// R-run Task 8 (PR-045/PR-006): a `repeat(N) body;` loop's `header`
600    /// terminator, when `body` contains a suspend point (see
601    /// [`RepeatCounterId`]'s own doc comment for why this is a distinct
602    /// shape from `Branch`, never a fabricated `cond: Expression`). Mirrors
603    /// `while`'s own `header`/`Branch` role exactly -- `then_blk` is the
604    /// loop body, `else_blk` is the loop's exit -- except the re-checked
605    /// condition is `counter != 0` against this loop's own compiler-
606    /// synthesized [`RepeatCounterId`] slot, not a re-evaluated user
607    /// expression.
608    RepeatCheck {
609        counter: RepeatCounterId,
610        then_blk: BlockId,
611        else_blk: BlockId,
612    },
613    /// A `case`/`casez`/`casex`/`case ... inside` statement whose arms
614    /// cannot stay one straight-line leaf (an arm suspends, or `break`s /
615    /// `continue`s an enclosing process loop, or `return`s from an inlined
616    /// task): `stmt` supplies the selector and labels, matched in item
617    /// order, `arms[i]` is `stmt.case_items()[i]`'s block and `default_blk`
618    /// runs when no item matches. Also a `unique`/`unique0`/`priority` `if`
619    /// series: `stmt` is its first `if` ([`if_chain`]), `arms[i]` the
620    /// then-statement of the i-th condition and `default_blk` the final `else`,
621    /// so the dispatch can report a violation where the series executes.
622    /// Also an `if` with a pattern-matching or `&&&`-chained predicate (one
623    /// arm, [`is_clause_if`]) and a `case ... matches` (`arms[i]` is
624    /// `pattern_case_items()[i]`'s block, `default_blk` its `default`).
625    Case {
626        stmt: Statement<'d>,
627        arms: Vec<BlockId>,
628        default_blk: BlockId,
629    },
630}
631
632/// One argument transfer of a task call inlined into the process CFG
633/// (`CfgBuilder::inline_task_call`): a task whose body can suspend (`@`, `#`,
634/// `wait`, ...) cannot be an ordinary compiled function, so its body is
635/// walked in place and these run at the start of the inlined region's
636/// blocks. Every symbol named here is one of `ProcessCfg::inlined_locals`.
637#[derive(Debug, Clone)]
638pub enum ArgBinding<'d> {
639    /// `formal = actual`: an input (or the copy-in of an inout) formal.
640    In {
641        formal: Symbol<'d>,
642        actual: Expression<'d>,
643    },
644    /// `var` = its declaration initializer, or its type's default: an
645    /// `output` formal or one of the task's own locals, at task entry.
646    Init { var: Symbol<'d> },
647    /// `actual = formal`: an output/inout formal's copy-back after the body
648    /// (`actual` is the lvalue expression).
649    Out {
650        actual: Expression<'d>,
651        formal: Symbol<'d>,
652    },
653    /// `var = value`: a `foreach` loop variable's starting value.
654    Const { var: Symbol<'d>, value: i64 },
655    /// `var` = the current object: the `this` of a class-method task inlined
656    /// for an implicit-`this` call (`run()` inside another method), bound in
657    /// the caller's context. An explicit receiver (`obj.run()`) binds through
658    /// [`ArgBinding::In`] instead.
659    ThisOf { var: Symbol<'d> },
660    /// An inlined task's input formal or entry-initialized local ending
661    /// with the call (an automatic variable ends with its scope, LRM 6.21):
662    /// whatever class / collection handles it holds are released (LRM
663    /// 13.5.1). Runs at the call's exit block, so `return` and `disable`
664    /// release too.
665    Release { var: Symbol<'d> },
666    /// `var += by`: a `foreach` loop variable's per-iteration step.
667    Step { var: Symbol<'d>, by: i64 },
668    /// `void'(arr.first(var))`, or with `next` `void'(arr.next(var))`: a
669    /// `foreach` over the associative array `arr` walking its keys.
670    ///
671    /// For an associative array nested in a collection (`int m[int][string]`,
672    /// `int q[$][int]`), `path` holds the enclosing `foreach` levels that
673    /// select the array from `arr` (as [`RepeatCount::NestedSize`]); empty
674    /// for `arr` itself.
675    AssocKey {
676        var: Symbol<'d>,
677        arr: Expression<'d>,
678        path: Vec<NestStep<'d>>,
679        next: bool,
680    },
681    /// `lhs = #d rhs;` and the other intra-assignment timing controls (LRM
682    /// 1800-2023 §10.4.1): the right-hand side is evaluated before the wait
683    /// into the compiler temporary `temp` (a `FrameSlotSource::Temporary`
684    /// frame slot, sized by [`ProcessCfg::captures`]`[temp]`)...
685    Capture { temp: u32, value: Expression<'d> },
686    /// ...and assigned to its target after it.
687    ///
688    /// With `nba` (`lhs <= @(e) rhs;`, LRM 1800-2023 §10.4.2) the assignment
689    /// is a nonblocking one, staged for the NBA region: it runs in the
690    /// synthetic fork branch [`CfgBuilder::walk_nba_event`] spawns.
691    AssignCaptured {
692        temp: u32,
693        target: Expression<'d>,
694        nba: bool,
695    },
696    /// `formal = temp`: a recursive task's input formal, copied from the
697    /// call's evaluated argument ([`InheritedSlot::Temp`]) as the forked
698    /// activation starts ([`CfgBuilder::inline_recursive_task`]).
699    FromCaptured { temp: u32, formal: Symbol<'d> },
700    /// `temp = formal`: a recursive task's `output`/`inout` formal, parked in
701    /// a compiler temporary as the activation ends so the spawner can copy
702    /// it back ([`InheritedSlot::Return`]).
703    ToCaptured { temp: u32, formal: Symbol<'d> },
704    /// Marks capture temporary `temp` as the value of the inlined call `call`
705    /// of a function that spawns processes: the call's expression reads the
706    /// temporary (keyed by [`call_result_key`]) instead of the function's
707    /// shared result variable, which a second call of the same function in
708    /// the statement, or another override of a virtual method, overwrites.
709    /// A no-op at run time; [`ArgBinding::ToCaptured`] parks the value.
710    CallResult { temp: u32, call: Expression<'d> },
711    /// The resume of a mixed `@(class_event or signal ...)` wait (LRM 1800-2023
712    /// 9.4.2.1, 15.5): the process leaves the class-event queues it is still
713    /// queued on (the signal side woke it), so a later trigger of one of those
714    /// events cannot wake a wait it is no longer in.
715    ClassEventLeave,
716    /// `temp` = the caller's current object: the receiver slot of a recursive
717    /// class-method activation called with an implicit `this` (LRM 1800-2023
718    /// 8.11, 13.4.1); an explicit receiver is an ordinary
719    /// [`ArgBinding::Capture`].
720    CaptureThis { temp: u32 },
721    /// A `ref` formal bound to element `actual = c[i]` of a queue or dynamic
722    /// array (LRM 1800-2023 13.5.2, 7.10.3): opens a runtime reference to the
723    /// element into the 64-bit temporary `temp` (the collection's tracking
724    /// outdates it on delete / pop / clear / resize), then `formal` = the
725    /// element.
726    RefOpen {
727        temp: u32,
728        formal: Symbol<'d>,
729        actual: Expression<'d>,
730    },
731    /// The copy-out of [`ArgBinding::RefOpen`]: `actual` = `formal` unless
732    /// the reference `temp` was outdated meanwhile.
733    RefClose {
734        temp: u32,
735        formal: Symbol<'d>,
736        actual: Expression<'d>,
737    },
738}
739
740/// A frame slot a spawned branch copies by value from its parent
741/// ([`ProcessCfg::inherited`]).
742#[derive(Debug, Clone, Copy, PartialEq, Eq)]
743pub enum InheritedSlot {
744    /// Capture temporary `parent` of the forking process -> `child` of the branch.
745    Temp { parent: u32, child: u32 },
746    /// Repeat counter `parent` -> `child`.
747    Counter {
748        parent: RepeatCounterId,
749        child: RepeatCounterId,
750    },
751    /// The other way round, when the branch is done: its capture temporary
752    /// `child` -> temporary `parent` of the process that forked it (a
753    /// recursive task's `output`/`inout` formals, LRM 1800-2023 13.5).
754    Return { parent: u32, child: u32 },
755}
756
757/// What a [`Terminator::Switch`] selects on.
758#[derive(Clone)]
759pub enum SwitchOn<'d> {
760    /// The index of the wait term that woke the process at the suspend just
761    /// before (`sv_rt_fired_term`): `wait_order`.
762    FiredTerm,
763    /// Whether term `k` of the event wait just before matched (bit `k` of
764    /// `sv_rt_fired_mask`): 1 when it did, else 0. A class event queues every
765    /// term of an `@(a iff x or b iff y)` list on its own, and the resume
766    /// judges each fired term's `iff` (LRM 1800-2023 9.4.2.3, 15.5).
767    FiredBit(u32),
768    /// After a mixed `@(class_event or signal ...)` wait: whether class term
769    /// `k` of the list (its kth class event) woke the process (`Some(k)`), or
770    /// any class term did (`None`); 0 when a signal term woke it. The class
771    /// side keeps its own record (`sched::class_event::fired_bit`) because a
772    /// signal term's index would share the fired-term mask with it.
773    ClassFired(Option<u32>),
774    /// A virtual interface's instance id minus one (`netlist::vif`): an
775    /// interface task called through it.
776    VifId(Expression<'d>),
777    /// A `randcase`'s item, picked at random by the items' weights (none
778    /// when every weight is 0).
779    RandCase(Statement<'d>),
780    /// The arm whose class template ids hold the dynamic class of the object
781    /// `receiver` names (the current object when `None`): a virtual class
782    /// task that can suspend, one inlined override per arm (LRM 8.20).
783    ObjClass {
784        receiver: Option<Expression<'d>>,
785        ids: Vec<Vec<u32>>,
786    },
787    /// No arm is ever taken: the switch only reports its error (`what` is the
788    /// message) and continues at the default -- a call to an `extern forkjoin`
789    /// task no module implements (LRM 25.7.5).
790    NoTarget { what: String },
791    /// The arm of the named block (`labels[i]`, innermost first) that the
792    /// subroutine call just before disabled with `disable <block>;` from
793    /// inside a function (LRM 9.6.2, 35.9), else the default
794    /// (`sv_rt_dpi_fn_disable_index`).
795    FnDisable { labels: Vec<String> },
796}
797
798impl std::fmt::Debug for SwitchOn<'_> {
799    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
800        match self {
801            SwitchOn::NoTarget { what } => write!(f, "NoTarget({what})"),
802            SwitchOn::FnDisable { labels } => write!(f, "FnDisable({labels:?})"),
803            SwitchOn::FiredTerm => write!(f, "FiredTerm"),
804            SwitchOn::FiredBit(k) => write!(f, "FiredBit({k})"),
805            SwitchOn::ClassFired(k) => write!(f, "ClassFired({k:?})"),
806            SwitchOn::VifId(e) => write!(f, "VifId({:?})", e.kind()),
807            SwitchOn::RandCase(_) => write!(f, "RandCase"),
808            SwitchOn::ObjClass { ids, .. } => write!(f, "ObjClass({ids:?})"),
809        }
810    }
811}
812
813impl<'d> SwitchOn<'d> {
814    /// Every expression the selection reads.
815    pub fn reads(&self) -> Vec<Expression<'d>> {
816        match self {
817            SwitchOn::FiredTerm
818            | SwitchOn::FiredBit(_)
819            | SwitchOn::ClassFired(_)
820            | SwitchOn::NoTarget { .. }
821            | SwitchOn::FnDisable { .. } => Vec::new(),
822            SwitchOn::VifId(e) => vec![*e],
823            SwitchOn::RandCase(s) => s.randcase_items().iter().map(|i| i.expr()).collect(),
824            SwitchOn::ObjClass { receiver, .. } => receiver.iter().copied().collect(),
825        }
826    }
827}
828
829/// One enclosing `foreach` dimension on the way to a nested collection
830/// ([`RepeatCount::NestedSize`]): its index variable and, for a fixed-size
831/// dimension, its `(left, right)` bounds (`None`: a queue / dynamic array
832/// dimension, indexed from 0).
833#[derive(Debug, Clone, Copy)]
834pub struct NestStep<'d> {
835    pub var: Symbol<'d>,
836    pub fixed: Option<(i32, i32)>,
837    /// The dimension is an associative array: `var` holds a key, not a
838    /// position.
839    pub assoc: bool,
840}
841
842/// A counted loop's iteration count: a `repeat (N)` loop's count
843/// expression, a `foreach` loop's static element count, or the number of
844/// elements a queue or dynamic array has when its `foreach` starts.
845#[derive(Debug, Clone)]
846pub enum RepeatCount<'d> {
847    Expr(Expression<'d>),
848    Const(u64),
849    /// The size of this queue / dynamic array expression.
850    Size(Expression<'d>),
851    /// The size of the queue / dynamic array reached from `arr` by the
852    /// enclosing `foreach` indices `path` (`a[i][j]` of `foreach (a[i, j,
853    /// k])` over `int a[2][3][]`), read when the loop is entered.
854    NestedSize {
855        arr: Expression<'d>,
856        path: Vec<NestStep<'d>>,
857    },
858    /// A count the forking process already evaluated: the counter slot is
859    /// copied in at the spawn ([`InheritedSlot::Counter`]), never
860    /// initialized here. `0` is the count expression, which sizes the slot.
861    Inherited(Expression<'d>),
862}
863
864impl<'d> RepeatCount<'d> {
865    /// The expression the count is computed from, if the count is not a
866    /// constant (for `Size`, the collection).
867    pub fn expr(&self) -> Option<Expression<'d>> {
868        match self {
869            RepeatCount::Expr(e) | RepeatCount::Size(e) => Some(*e),
870            RepeatCount::NestedSize { arr, .. } => Some(*arr),
871            RepeatCount::Const(_) | RepeatCount::Inherited(_) => None,
872        }
873    }
874}
875
876/// A straight-line run of non-suspending statements ending in exactly one
877/// [`Terminator`] (PR-008).
878#[derive(Debug, Clone)]
879pub struct BasicBlock<'d> {
880    /// Inlined-task argument transfers, run before `stmts` (see
881    /// [`ArgBinding`]).
882    pub bindings: Vec<ArgBinding<'d>>,
883    /// The `this` of the innermost class-method task inlined at this block
884    /// (its body's blocks; its binding and continuation blocks belong to the
885    /// caller): what an implicit-`this` member access or method call in the
886    /// block resolves against when the process itself is not a method.
887    pub this_var: Option<Symbol<'d>>,
888    /// The `ref` formals of the tasks inlined at this block that name a
889    /// static variable or net of their call (LRM 13.5.2), with that storage:
890    /// the formal reads and writes the actual itself in this block, so one
891    /// process may bind the same formal to different actuals per call.
892    pub ref_aliases: Vec<(Symbol<'d>, Symbol<'d>)>,
893    pub stmts: Vec<Statement<'d>>,
894    /// VPI `cbStmt` probe points (IEEE 1800-2023 38.36.1): `(i, s)` means the
895    /// statement `s` is about to execute and the probe runs just before
896    /// `stmts[i]` (`i == stmts.len()` is after the last one, still before
897    /// `step_exprs` and the terminator). Only statements this CFG decomposes
898    /// (or keeps as a leaf, which codegen then does not probe again) appear
899    /// here; codegen emits a probe for them only when a VPI library is loaded.
900    pub probes: Vec<(usize, Statement<'d>)>,
901    /// VPI `cbDisable` probe points (38.36.1): `(i, s)` means the `disable`
902    /// statement `s`, which this CFG turned into a jump out of the named block
903    /// it names (so it is not in `stmts`), has taken effect; the probe runs
904    /// where `probes` at index `i` do, after them.
905    pub disable_probes: Vec<(usize, Statement<'d>)>,
906    /// VPI frame probe points (37.43) of the blocking tasks inlined into this
907    /// CFG: they run where `probes` at the same index do, after them (and
908    /// after any `disable_probes`).
909    pub frame_probes: Vec<(usize, FrameProbe<'d>)>,
910    /// Covergroup block events (LRM 19.3 `@@(begin t)` / `@@(end t)`) of the
911    /// blocking tasks inlined into this CFG: `(i, task, is_end)` signals the
912    /// begin (or end) of `task`'s activation where `probes` at index `i` run.
913    /// Codegen emits only those whose task a covergroup names.
914    pub block_events: Vec<(usize, Symbol<'d>, bool)>,
915    /// `(i, subroutine)`: `stmts[i]` was written in the body of the inlined `subroutine`, whose
916    /// time unit its delays (`q <= #d v`) are in (LRM 3.14.2.1). Absent for the process's own.
917    pub stmt_scopes: Vec<(usize, Symbol<'d>)>,
918    /// A `for` loop's per-iteration step expressions (e.g. `i++`), run after
919    /// `stmts` and before `terminator` fires (Task 1, `walk_for_loop`).
920    /// Empty for every block that is not a `for` loop's step block. A
921    /// separate field, not folded into `stmts: Vec<Statement<'d>>`, because
922    /// `Statement::for_loop_steps()` returns bare `Expression<'d>`s (`i++`
923    /// is an expression in this grammar position, never a `Statement` --
924    /// confirmed against the pinned `sv-lang`=0.1.9 binding, which has no
925    /// expression-to-statement wrapper) and this crate never fabricates AST
926    /// nodes. Every slot this task's `for` fixture writes through
927    /// (`i` itself) is a `FrameSlotSource::DeclaredVar` permanent slot
928    /// (`lower_to_segments`'s own doc comment), so omitting these from
929    /// `liveness::compute_liveness`'s backward read/write walk does not
930    /// under-approximate any frame-slot's live range -- codegen consumption
931    /// of this field is future work, tracked in this task's deviations, not
932    /// a silent drop (the expressions are still here, in the CFG, for
933    /// whichever pass reads `step_exprs` next).
934    pub step_exprs: Vec<Expression<'d>>,
935    /// R-run Task 8: `Some((counter, count_expr))` iff this block is a
936    /// suspend-containing `repeat(N)` loop's own entry block -- `count_expr`
937    /// (`RepeatLoop::condition()`, per LRM the loop's iteration COUNT, not a
938    /// boolean) is evaluated exactly ONCE, run after `stmts` and before
939    /// `terminator` fires (mirrors [`Self::step_exprs`]'s own "run after
940    /// stmts, before terminator" placement), its result (clamped to
941    /// non-negative) becoming `counter`'s starting value. `None` for every
942    /// other block, including a suspend-free `repeat` (which stays the
943    /// pre-existing opaque leaf `stmt_shared::lower_repeat_loop` already
944    /// handles -- see [`RepeatCounterId`]'s own doc comment).
945    pub repeat_init: Option<(RepeatCounterId, RepeatCount<'d>)>,
946    /// R-run Task 8: `Some(counter)` iff this block is a suspend-containing
947    /// `repeat(N)` loop's body's own back-edge block -- decrements
948    /// `counter` by exactly one, run after `stmts`/`step_exprs` and before
949    /// `terminator` fires. `None` for every other block.
950    pub repeat_decrement: Option<RepeatCounterId>,
951    /// `true` iff this block is the one an `expect` statement's wait resumes
952    /// into (LRM 1800-2023 §16.17): its first statement is that `expect` again,
953    /// whose resume half (the pass/`else` action, selected by the outcome the
954    /// process was woken with) codegen lowers there.
955    pub expect_resume: bool,
956    pub terminator: Terminator<'d>,
957}
958
959/// PR-004, PR-008: the CFG built by [`build_cfg`] -- one LLVM function per
960/// process (Task S3c) will eventually `switch` on `resume_pc` with one case
961/// per segment derived from `blocks` below.
962#[derive(Debug, Clone)]
963pub struct ProcessCfg<'d> {
964    pub blocks: Vec<BasicBlock<'d>>,
965    pub entry: BlockId,
966    /// The process body's original root `Statement` (Task S3b deviation,
967    /// not in Task S3a's original field list): `process::liveness::
968    /// compute_liveness`'s declared-local discovery needs to walk the
969    /// *pre-flattening* statement tree looking for `Block` statements'
970    /// `block_symbol()` scopes -- `blocks`/`stmts` above only ever retain
971    /// the leaf statements `CfgBuilder::walk_seq` flattened into each
972    /// `BasicBlock`, which discards every intervening `begin`/`end` `Block`
973    /// statement node (and, with it, that block's `StatementBlockSymbol` --
974    /// the ONLY place `sv-lang`=0.1.9's pinned C API exposes a declared
975    /// local's `Symbol`; there is no per-`VariableDeclaration`-statement
976    /// symbol accessor at all, confirmed against the real binding). Keeping
977    /// the untouched root here, alongside the flattened `blocks`, is the
978    /// minimal way to make both queries possible without re-walking from
979    /// the caller.
980    pub body: Statement<'d>,
981    /// The formals and locals of every task inlined into this CFG
982    /// ([`ArgBinding`]) -- declared locals of the process for frame-layout
983    /// purposes, though no `Block` under `body` declares them.
984    pub inlined_locals: Vec<Symbol<'d>>,
985    /// The target of each intra-assignment-timed assignment's captured
986    /// right-hand side ([`ArgBinding::Capture`]), indexed by `temp`: its
987    /// type sizes the temporary's frame slot.
988    pub captures: Vec<Expression<'d>>,
989    /// For a fork branch: the variables of the forking activation it uses
990    /// (LRM 1800-2023 §9.3.2, §6.21) -- shared with it, not copied: their
991    /// frame slots, here and in the parent, hold a pointer to one cell.
992    pub captured: Vec<Symbol<'d>>,
993    /// `(formal, actual)`: a `ref` formal of an inlined task that every call
994    /// in this process binds to the same static variable or net (LRM
995    /// 1800-2023 §13.5.2). The formal names the actual's storage -- it has no
996    /// frame slot and no copy-in/copy-out -- so a wait on it (`@(posedge
997    /// clk)` with `ref logic clk`) sees the actual change.
998    pub ref_bindings: Vec<(Symbol<'d>, Symbol<'d>)>,
999    /// The name(s) a `disable <label>;` ANYWHERE (this process, a sibling
1000    /// branch, an ancestor fork, or a genuinely different process) may
1001    /// single this CFG's own live process out by (LRM 9.6.2): the
1002    /// enclosing `fork : NAME`'s name, if this CFG is one of its branches,
1003    /// and/or this CFG's own named outermost block (`peer_block(body)`),
1004    /// if it has one. Empty when nothing could ever name this process this
1005    /// way. `vj-codegen::process_lower`'s fork/join lowering copies this
1006    /// into each branch's own [`super::super::sched::SpawnSpec::
1007    /// disable_name`].
1008    pub disable_names: Vec<String>,
1009    /// Every name [`Self::disable_names`] of a fork branch this CFG's own
1010    /// walk resolved a `disable <label>;` against WITHOUT a same-process
1011    /// lexical match (not an enclosing [`BasicBlock`], not this CFG's own
1012    /// `fork_name`, not a procedural-assertion label): the kept `disable`
1013    /// statement's codegen (`vj-codegen::process_lower`) reads this to
1014    /// tell such a statement apart from the ones that resolve to the OLDER,
1015    /// same-fork-only `sv_rt_disable_fork_block` path -- see
1016    /// [`CfgBuilder::fork_branch_names`]'s own doc comment for how it
1017    /// accumulates (parent-to-branch, branch-to-branch, and forward past
1018    /// the fork statement that spawned them).
1019    pub fork_branch_names: Vec<String>,
1020    /// `(resume, label, exit)`: while the process is suspended and will resume
1021    /// at block `resume`, it is inside the named block or task activation
1022    /// `label`, which a `disable label;` of another process ends by resuming
1023    /// the process at block `exit` instead (LRM 9.6.2). Only labels some
1024    /// `disable` of the design names ([`is_disabled_somewhere`]) are listed.
1025    /// A segment index is a valid resume point of the compiled kernel.
1026    pub disable_scopes: Vec<(BlockId, String, BlockId)>,
1027    /// The labels of the `disable` statements this CFG keeps as statements
1028    /// because they must also reach the processes other than this one that
1029    /// are inside the named block or task (`sv_rt_disable_named`), with
1030    /// whether the statement is the whole effect of the disable (an
1031    /// unresolved target: no jump follows, so codegen emits the VPI
1032    /// `cbDisable` probe itself).
1033    pub disable_broadcast: Vec<(String, bool)>,
1034    /// For a synthetic fork branch ([`CfgBuilder::walk_nba_event`]): the
1035    /// frame slots of the forking process copied BY VALUE into this
1036    /// branch's own at the spawn (LRM 1800-2023 §10.4.2: the right-hand
1037    /// side and the repeat count are evaluated when the assignment is
1038    /// reached, not when its event fires).
1039    pub inherited: Vec<InheritedSlot>,
1040    /// Per capture temporary (`captures`), the formal whose type sizes its
1041    /// frame slot instead of the captured expression's (a recursive task's
1042    /// call arguments, [`CfgBuilder::inline_recursive_task`]); entries past
1043    /// the end are `None`.
1044    pub capture_types: Vec<Option<Symbol<'d>>>,
1045    /// Whether this CFG is a recursive task's activation ([`Recursion`]).
1046    pub recursion: Recursion,
1047    /// Two fork branches carrying the same id were built from the same
1048    /// statement in the same inherited context ([`ForkBranchKey`]), so
1049    /// they are the same CFG and lower to the same kernel: the lowering
1050    /// compiles it once per process and points every spawn at that code
1051    /// (a UVM testbench otherwise cloned each branch once per path of
1052    /// nested forks leading to it, tens of GB of IR). `None` for a branch
1053    /// that is not shared.
1054    pub share_id: Option<u64>,
1055}
1056
1057/// A fork branch's part in a recursive task with timing controls (LRM
1058/// 1800-2023 §13.4, §13.4.1): every call is a `join` of one forked child
1059/// running the task body in a frame of its own.
1060#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1061pub enum Recursion {
1062    /// An ordinary CFG.
1063    None,
1064    /// The task's body: the branch the first call forks. Its kernel is
1065    /// compiled once and also serves every recursive call inside it.
1066    Root,
1067    /// A placeholder branch (no blocks of its own): a recursive call inside
1068    /// the enclosing [`Recursion::Root`], which spawns that root's kernel again.
1069    SelfSpawn,
1070    /// A placeholder branch: a call, inside one recursive activation, of the
1071    /// task of an enclosing activation (mutual recursion: `a` calls `b` calls
1072    /// `a`). It spawns that activation's kernel again; the number says how
1073    /// many [`Recursion::Root`] levels out it is (1 = the activation this one
1074    /// was forked from).
1075    AncestorSpawn(u32),
1076}
1077
1078/// Whether the process re-triggers from `entry` after finishing one pass
1079/// through its body (`always`/`always_ff`/`always_comb`/`always_latch`) or
1080/// runs exactly once (`initial`/`final`) -- PR-048's `Terminator::End{
1081/// loop_back}` distinction. Deliberately **not**
1082/// `vj-codegen::module::SliceProcKind`: this crate must never depend on
1083/// `vj-codegen` (see [`LoweringError`]'s doc comment for the same
1084/// reasoning); a caller that already has a `SliceProcKind` (module
1085/// discovery, Task S2/S7) maps it down to this coarser 2-value distinction
1086/// itself (`Initial` -> `OneShot`, `Always`/`AlwaysFF` -> `Looping`).
1087#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1088pub enum ProcessLoopKind {
1089    OneShot,
1090    Looping,
1091}
1092
1093/// PR-007: the single recursive-descent CFG builder entry point.
1094pub fn build_cfg<'d>(
1095    body: &Statement<'d>,
1096    loop_kind: ProcessLoopKind,
1097) -> Result<ProcessCfg<'d>, LoweringError> {
1098    build_cfg_in(
1099        body,
1100        loop_kind,
1101        None,
1102        None,
1103        None,
1104        None,
1105        Inherited::default(),
1106        None,
1107    )
1108}
1109
1110/// The CFG of a one-shot process that runs the body of `task` once, for a
1111/// task exported to C (`export "DPI-C" task`, LRM 1800-2023 35.7) whose body
1112/// consumes time (or stages a nonblocking assignment): C cannot run it as a
1113/// function, so each call from C spawns a process of this CFG and the
1114/// importing task blocks until it finishes (35.5.3).
1115///
1116/// The formals are ordinary locals of the process's frame, with NO entry
1117/// binding: the spawner stores the C arguments into their frame slots before
1118/// the process starts and reads the `output`/`inout` ones back when it ends
1119/// (the process's frame is what a copy-in/copy-out call has in a caller).
1120/// Other automatic locals get their declaration initialisers as usual.
1121/// `static` formals are module state, not frame slots; they stay out of
1122/// `inlined_locals`, exactly as a call inlined into a process treats them.
1123///
1124/// Fails by name for a task that calls itself (a recursive activation needs
1125/// the fork-per-call machinery of [`Recursion::Root`], whose argument
1126/// capture an export cannot supply).
1127pub fn build_export_task_cfg<'d>(
1128    task: &Symbol<'d>,
1129    global_clock: Option<InferredClockEvent<'d>>,
1130) -> Result<ProcessCfg<'d>, LoweringError> {
1131    let body = proc_body(task).ok_or_else(|| {
1132        LoweringError::malformed(format!("exported task `{}` has no body", task.name()))
1133    })?;
1134    let mut b = CfgBuilder::new();
1135    b.global_clock = global_clock;
1136    let entry = b.alloc_block();
1137    // Calling itself from its own body would inline without end; the
1138    // recursion machinery needs a caller's actuals to type its captures.
1139    b.rec_outer.push(task.uid());
1140    for f in task.subroutine_arguments() {
1141        if !crate::netlist::is_static_procedural_var(&f) {
1142            b.inlined_locals.push(f);
1143        }
1144    }
1145    for local in task_locals(task) {
1146        if crate::netlist::is_static_procedural_var(&local) {
1147            continue;
1148        }
1149        if !is_automatic_block_var(&local) {
1150            b.blocks[entry.0 as usize]
1151                .bindings
1152                .push(ArgBinding::Init { var: local });
1153        }
1154        b.inlined_locals.push(local);
1155    }
1156    for v in crate::patterns::pattern_vars(body.as_sem_node()) {
1157        if !b.inlined_locals.iter().any(|l| l.uid() == v.uid()) {
1158            b.inlined_locals.push(v);
1159        }
1160    }
1161    let after = b.alloc_block();
1162    b.target_depth.insert(after, 0);
1163    b.return_targets.push(after);
1164    let exit = b.walk_stmt(body, entry)?;
1165    b.close(exit, Terminator::Fallthrough(after));
1166    b.close(after, Terminator::End { loop_back: None });
1167    let inlined_locals = std::mem::take(&mut b.inlined_locals);
1168    let captures = std::mem::take(&mut b.captures);
1169    let capture_types = std::mem::take(&mut b.capture_types);
1170    let ref_bindings = b.take_ref_bindings();
1171    let disable_scopes = b.take_disable_scopes();
1172    let disable_broadcast = std::mem::take(&mut b.disable_broadcast);
1173    let fork_branch_names = std::mem::take(&mut b.fork_branch_names);
1174    Ok(ProcessCfg {
1175        blocks: b.finish(),
1176        entry,
1177        body,
1178        inlined_locals,
1179        captures,
1180        captured: Vec::new(),
1181        ref_bindings,
1182        disable_names: Vec::new(),
1183        fork_branch_names,
1184        disable_scopes,
1185        disable_broadcast,
1186        inherited: Vec::new(),
1187        capture_types,
1188        recursion: Recursion::None,
1189        share_id: None,
1190    })
1191}
1192
1193/// The one-shot process a task called from a sequence match item runs as
1194/// (LRM 1800-2023 16.11: "the subroutine call is scheduled in the Reactive
1195/// region ... as a thread of its own"). Its body is the call `call` (a user
1196/// task that can suspend, [`subroutine_can_suspend`]) inlined the way a call
1197/// statement of an ordinary process is: the formals are bound from the actual
1198/// expressions by [`ArgBinding::In`] in the entry block, so the process reads
1199/// its by-value actuals when it starts (the assertion kernel lowers each as the
1200/// value the engine sampled at the end point, `sva::proc::call_arg_key`) and
1201/// every call is its own activation with its own frame.
1202///
1203/// Fails by name for a task that calls itself (a recursive activation needs the
1204/// fork-per-call machinery of [`Recursion::Root`], whose argument capture a
1205/// spawned call cannot supply).
1206pub fn build_match_item_task_cfg<'d>(
1207    call: Expression<'d>,
1208) -> Result<ProcessCfg<'d>, LoweringError> {
1209    let task = call.call_subroutine().ok_or_else(|| {
1210        LoweringError::malformed("a match-item call of a task has no subroutine symbol")
1211    })?;
1212    let body = proc_body(&task)
1213        .ok_or_else(|| LoweringError::malformed(format!("task `{}` has no body", task.name())))?;
1214    let mut b = CfgBuilder::new();
1215    let entry = b.alloc_block();
1216    let exit = b.inline_task_call(call, task, entry)?;
1217    b.close(exit, Terminator::End { loop_back: None });
1218    let inlined_locals = std::mem::take(&mut b.inlined_locals);
1219    let captures = std::mem::take(&mut b.captures);
1220    let capture_types = std::mem::take(&mut b.capture_types);
1221    let ref_bindings = b.take_ref_bindings();
1222    let disable_scopes = b.take_disable_scopes();
1223    let disable_broadcast = std::mem::take(&mut b.disable_broadcast);
1224    let fork_branch_names = std::mem::take(&mut b.fork_branch_names);
1225    Ok(ProcessCfg {
1226        blocks: b.finish(),
1227        entry,
1228        body,
1229        inlined_locals,
1230        captures,
1231        captured: Vec::new(),
1232        ref_bindings,
1233        disable_names: Vec::new(),
1234        fork_branch_names,
1235        disable_scopes,
1236        disable_broadcast,
1237        inherited: Vec::new(),
1238        capture_types,
1239        recursion: Recursion::None,
1240        share_id: None,
1241    })
1242}
1243
1244/// The clocking event a checker's `$inferred_clock` (LRM 17.4) stands for: the
1245/// signal and edge of the default clocking of the scope that instantiates the
1246/// checker (14.12), which a checker procedure's `@clock` waits on when `clock`
1247/// kept its default. The third member is the event's `iff` condition
1248/// (`default clocking @(posedge clk iff en);`, 9.4.2.3), which gates every wait
1249/// on it; `None` for `@$global_clock`.
1250pub type InferredClockEvent<'d> = (Expression<'d>, EdgeKind, Option<Expression<'d>>);
1251
1252/// Whether `signal` (the expression of a signal event control) is a call of
1253/// `$global_clock` (14.14).
1254pub fn is_global_clock_call(signal: &Expression<'_>) -> bool {
1255    signal.kind() == ExpressionKind::Call
1256        && signal.is_system_call() == Some(true)
1257        && signal
1258            .system_subroutine()
1259            .is_some_and(|s| s.known_name_id() == sv_lang::KnownSystemName::GlobalClock)
1260}
1261
1262/// Whether `signal` (the expression of a signal event control) is a call of
1263/// `$inferred_clock`: a checker's `event clock = $inferred_clock` formal, used as
1264/// `@clock` by a procedure of an instance that did not bind it (17.4).
1265pub fn is_inferred_clock_call(signal: &Expression<'_>) -> bool {
1266    signal.kind() == ExpressionKind::Call
1267        && signal.is_system_call() == Some(true)
1268        && signal
1269            .system_subroutine()
1270            .is_some_and(|s| s.known_name_id() == sv_lang::KnownSystemName::InferredClock)
1271}
1272
1273/// The clocking block of a drive target rooted in a clock variable
1274/// (`cb.q`, `cb.q[3:0]`; LRM 1800-2023 14.16), else `None`.
1275fn drive_clocking_of<'d>(mut target: Expression<'d>) -> Option<Symbol<'d>> {
1276    while matches!(
1277        target.kind(),
1278        ExpressionKind::ElementSelect | ExpressionKind::RangeSelect
1279    ) {
1280        target = target.select_value()?;
1281    }
1282    let var = target.referenced_symbol()?;
1283    if var.kind() != SymbolKind::ClockVar {
1284        return None;
1285    }
1286    var.parent()
1287        .filter(|p| p.kind() == SymbolKind::ClockingBlock)
1288}
1289
1290/// The clock variable of a drive target rooted in a virtual interface member
1291/// access (`v.sb.c`, `v.sb.c[3:0]`), else `None`.
1292fn drive_via_of<'d>(mut target: Expression<'d>) -> Option<Expression<'d>> {
1293    while matches!(
1294        target.kind(),
1295        ExpressionKind::ElementSelect | ExpressionKind::RangeSelect
1296    ) {
1297        target = target.select_value()?;
1298    }
1299    (crate::netlist::vif::is_member_access(&target)).then_some(target)
1300}
1301
1302/// [`build_cfg`] for a process of an instance whose default clocking block
1303/// is `default_clocking` (what `##N` counts).
1304pub fn build_cfg_clocked<'d>(
1305    body: &Statement<'d>,
1306    loop_kind: ProcessLoopKind,
1307    default_clocking: Option<Symbol<'d>>,
1308) -> Result<ProcessCfg<'d>, LoweringError> {
1309    build_cfg_scoped(body, loop_kind, default_clocking, &[], None)
1310}
1311
1312/// [`build_cfg_clocked`] for a process that shares its instance with the
1313/// processes whose outermost named blocks are `peers`: a `disable` of one of
1314/// them is accepted when it cannot be inside the block (see [`PeerBlock`]).
1315/// `inferred_clock` is the event a checker procedure's `@clock` waits on when its
1316/// `clock` formal kept the default `$inferred_clock` (17.4); `None` for a process
1317/// that is not a checker's or whose instantiation context infers no clock.
1318pub fn build_cfg_scoped<'d>(
1319    body: &Statement<'d>,
1320    loop_kind: ProcessLoopKind,
1321    default_clocking: Option<Symbol<'d>>,
1322    peers: &[PeerBlock],
1323    inferred_clock: Option<InferredClockEvent<'d>>,
1324) -> Result<ProcessCfg<'d>, LoweringError> {
1325    build_cfg_in(
1326        body,
1327        loop_kind,
1328        None,
1329        default_clocking,
1330        Some(peers),
1331        inferred_clock,
1332        Inherited::default(),
1333        None,
1334    )
1335}
1336
1337/// [`build_cfg_scoped`] that can dispatch a virtual class task that suspends
1338/// over the class templates of `virtual_tasks`.
1339pub fn build_cfg_scoped_with_virtuals<'d>(
1340    body: &Statement<'d>,
1341    loop_kind: ProcessLoopKind,
1342    default_clocking: Option<Symbol<'d>>,
1343    peers: &[PeerBlock],
1344    inferred_clock: Option<InferredClockEvent<'d>>,
1345    global_clock: Option<InferredClockEvent<'d>>,
1346    virtual_tasks: Option<std::rc::Rc<VirtualTasks<'d>>>,
1347) -> Result<ProcessCfg<'d>, LoweringError> {
1348    build_cfg_in(
1349        body,
1350        loop_kind,
1351        None,
1352        default_clocking,
1353        Some(peers),
1354        inferred_clock,
1355        Inherited {
1356            global_clock,
1357            ..Inherited::default()
1358        },
1359        virtual_tasks,
1360    )
1361}
1362
1363/// The outermost named block of another process of the instance, which a
1364/// `disable` may target (LRM 9.6.2, 16.14.6.4).
1365#[derive(Clone, Debug, PartialEq, Eq)]
1366pub struct PeerBlock {
1367    pub name: String,
1368    /// The block contains a suspend point, so its process may be inside it. A
1369    /// `disable` then has to terminate the process's activity (9.6.2), which
1370    /// the scheduler cannot do; when the block cannot suspend, the process is
1371    /// never inside it while another runs, and the disable's only effect is the
1372    /// flush of the process's pending procedural assertion queue.
1373    pub can_suspend: bool,
1374    /// The block is the outermost block of a fork branch (its own process,
1375    /// tagged by name): any number of those may share a name, and `disable
1376    /// <name>;` ends every one (LRM 9.6.2).
1377    pub fork_branch: bool,
1378}
1379
1380/// The outermost scope of a process body (LRM 16.14.6.2): the block statement
1381/// the body is, looking through the leading timing controls of an `always`
1382/// (`always @(a or b) begin : b2 ... end`). `None` when the body is not a block.
1383pub fn outermost_block<'d>(body: &Statement<'d>) -> Option<Statement<'d>> {
1384    let mut cur = *body;
1385    loop {
1386        // Not a `match` over the kind: every other statement kind means "the body
1387        // is not a block", which is an answer, not an unimplemented case.
1388        let kind = cur.kind();
1389        if kind == StatementKind::Block {
1390            return Some(cur);
1391        }
1392        if kind == StatementKind::Timed {
1393            cur = cur.body()?;
1394        } else if kind == StatementKind::List {
1395            let list = live_statements(&cur);
1396            let [only] = list.as_slice() else {
1397                return None;
1398            };
1399            cur = *only;
1400        } else {
1401            return None;
1402        }
1403    }
1404}
1405
1406/// The named outermost block of the process body `body`, as another process
1407/// sees it ([`PeerBlock`]); `None` for an unnamed one.
1408pub fn peer_block<'d>(body: &Statement<'d>) -> Option<PeerBlock> {
1409    let block = outermost_block(body)?;
1410    let name = block_name(&block);
1411    if name.is_empty() {
1412        return None;
1413    }
1414    Some(PeerBlock {
1415        name,
1416        can_suspend: contains_suspend_trigger_in(
1417            block.as_sem_node(),
1418            &mut std::collections::HashSet::new(),
1419            true,
1420            false,
1421        ),
1422        fork_branch: false,
1423    })
1424}
1425
1426/// The hierarchical path of every named block (sequential or fork) in the
1427/// process body `body`, dotted from its outermost named block down
1428/// (`a`, `a.b`, `a.b.c`, `a.f.br1`): what a `disable s.a.b.c.inner;` from
1429/// another instance names, however deep (LRM 9.6.2, 23.6).
1430pub fn named_block_paths(body: &Statement<'_>) -> Vec<String> {
1431    let mut out = Vec::new();
1432    let mut stack: Vec<(SemNode<'_>, String)> = vec![(body.as_sem_node(), String::new())];
1433    while let Some((n, prefix)) = stack.pop() {
1434        let mut here = prefix.clone();
1435        if let Some(s) = n.as_statement()
1436            && s.kind() == StatementKind::Block
1437        {
1438            let name = block_name(&s);
1439            if !name.is_empty() {
1440                here = if prefix.is_empty() {
1441                    name
1442                } else {
1443                    format!("{prefix}.{name}")
1444                };
1445                out.push(here.clone());
1446            }
1447        }
1448        stack.extend(live_children(&n).into_iter().map(|c| (c, here.clone())));
1449    }
1450    out
1451}
1452
1453/// The named sequential blocks nested in the process body `body` below its
1454/// outermost scope, as another process sees them ([`PeerBlock`]): a `disable`
1455/// of one of them from another process ends the activity inside it (LRM
1456/// 9.6.2). A fork's branches are processes of their own and are not entered.
1457pub fn nested_peer_blocks<'d>(body: &Statement<'d>) -> Vec<PeerBlock> {
1458    let outermost = outermost_block(body).map(|b| b.source_range().start.offset);
1459    let mut out = Vec::new();
1460    let mut stack = vec![body.as_sem_node()];
1461    while let Some(n) = stack.pop() {
1462        if let Some(s) = n.as_statement()
1463            && s.kind() == StatementKind::Block
1464        {
1465            match s.block_kind() {
1466                Some(StatementBlockKind::Sequential) => {
1467                    let name = block_name(&s);
1468                    if !name.is_empty() && outermost != Some(s.source_range().start.offset) {
1469                        out.push(PeerBlock {
1470                            name,
1471                            can_suspend: contains_suspend_trigger_in(
1472                                n,
1473                                &mut std::collections::HashSet::new(),
1474                                true,
1475                                false,
1476                            ),
1477                            fork_branch: false,
1478                        });
1479                    }
1480                }
1481                Some(
1482                    StatementBlockKind::JoinAll
1483                    | StatementBlockKind::JoinAny
1484                    | StatementBlockKind::JoinNone,
1485                ) => {
1486                    fork_branch_peers(n, &mut out);
1487                    continue;
1488                }
1489                None => {}
1490            }
1491        }
1492        stack.extend(live_children(&n));
1493    }
1494    out
1495}
1496
1497/// The named outermost blocks of the branches of the fork `fork` and of every
1498/// fork nested in those branches: each branch is a process of its own tagged
1499/// with its block's name ([`ProcessCfg::disable_names`]), which `disable
1500/// <name>;` of another process ends (LRM 9.6.2). A branch's process may be
1501/// anywhere, so these always `can_suspend`. Named blocks nested deeper in a
1502/// branch are listed as ordinary nested peers.
1503fn fork_branch_peers(fork: SemNode<'_>, out: &mut Vec<PeerBlock>) {
1504    let mut branches = fork.children();
1505    if let [only] = branches.as_slice()
1506        && only
1507            .as_statement()
1508            .is_some_and(|s| s.kind() == StatementKind::List)
1509    {
1510        branches = only.children();
1511    }
1512    for b in branches {
1513        let Some(s) = b.as_statement() else { continue };
1514        if let Some(mut p) = peer_block(&s) {
1515            p.can_suspend = true;
1516            p.fork_branch = true;
1517            out.push(p);
1518        }
1519        // Blocks nested in the branch (and in forks nested in it) are blocks
1520        // of the branch's process: its kernel carries their disable scopes
1521        // (`ProcessCfg::disable_scopes`, `ForkKernel::disable_scopes`).
1522        out.extend(nested_peer_blocks(&s));
1523    }
1524}
1525
1526impl ProcessCfg<'_> {
1527    /// The labels of the disable scopes of every fork branch under this CFG
1528    /// (nested forks included): the blocks of the branch kernels a
1529    /// `disable` of another process can end (LRM 9.6.2).
1530    pub fn branch_disable_labels(&self) -> Vec<String> {
1531        let mut out = Vec::new();
1532        for b in &self.blocks {
1533            if let Terminator::ForkJoin { branches, .. } = &b.terminator {
1534                for br in branches {
1535                    out.extend(br.disable_scopes.iter().map(|(_, l, _)| l.clone()));
1536                    out.extend(br.branch_disable_labels());
1537                }
1538            }
1539        }
1540        out
1541    }
1542
1543    /// The `disable` broadcast targets of every fork branch under this CFG
1544    /// (nested forks included), as [`ProcessCfg::disable_broadcast`]: a
1545    /// `disable u1.blk;` inside a branch must resolve as one in the process
1546    /// itself does, or be refused (LRM 9.6.2).
1547    pub fn branch_disable_broadcast(&self) -> Vec<(String, bool)> {
1548        let mut out = Vec::new();
1549        for b in &self.blocks {
1550            if let Terminator::ForkJoin { branches, .. } = &b.terminator {
1551                for br in branches {
1552                    out.extend(br.disable_broadcast.iter().cloned());
1553                    out.extend(br.branch_disable_broadcast());
1554                }
1555            }
1556        }
1557        out
1558    }
1559}
1560
1561/// The label of every procedural concurrent assertion (not an `expect`, which
1562/// blocks) in the subtree of `body`, with the assertion statement: a labeled
1563/// statement is a block of that name holding it, which `disable <label>;`
1564/// names (16.14.6.4).
1565pub fn assertion_labels<'d>(body: &Statement<'d>) -> Vec<(String, Statement<'d>)> {
1566    let mut out = Vec::new();
1567    let mut stack = vec![body.as_sem_node()];
1568    while let Some(n) = stack.pop() {
1569        if let Some(s) = n.as_statement()
1570            && s.kind() == StatementKind::Block
1571            && let name = block_name(&s)
1572            && !name.is_empty()
1573            && let [only] = s.statements().as_slice()
1574            && only.kind() == StatementKind::ConcurrentAssertion
1575            && !is_expect(only)
1576        {
1577            out.push((name, *only));
1578        }
1579        stack.extend(live_children(&n));
1580    }
1581    out
1582}
1583
1584/// The label of every deferred immediate assertion (`L: assert #0 (c) ...;`,
1585/// `assert final`) in the subtree of `body`, with the assertion statement: a
1586/// labeled statement is a block of that name holding it, which `disable L;`
1587/// names (LRM 16.4.4: cancels that assertion's pending reports).
1588pub fn deferred_assertion_labels<'d>(body: &Statement<'d>) -> Vec<(String, Statement<'d>)> {
1589    let mut out = Vec::new();
1590    let mut stack = vec![body.as_sem_node()];
1591    while let Some(n) = stack.pop() {
1592        if let Some(s) = n.as_statement()
1593            && s.kind() == StatementKind::Block
1594            && let name = block_name(&s)
1595            && !name.is_empty()
1596            && let [only] = s.statements().as_slice()
1597            && only.kind() == StatementKind::ImmediateAssertion
1598            && only.is_deferred_assertion() == Some(true)
1599        {
1600            out.push((name, *only));
1601        }
1602        stack.extend(live_children(&n));
1603    }
1604    out
1605}
1606
1607/// The label of every plain (non-deferred) immediate assertion in the subtree
1608/// of `body`: a labeled statement is a block of that name holding it. A
1609/// `disable L;` outside that block (LRM 9.6.2) finds the block already
1610/// finished -- an immediate assertion's own block has no suspend point the
1611/// disable could catch from the same process.
1612pub fn immediate_assertion_labels(body: &Statement<'_>) -> Vec<String> {
1613    let mut out = Vec::new();
1614    let mut stack = vec![body.as_sem_node()];
1615    while let Some(n) = stack.pop() {
1616        if let Some(s) = n.as_statement()
1617            && s.kind() == StatementKind::Block
1618            && let name = block_name(&s)
1619            && !name.is_empty()
1620            && let [only] = s.statements().as_slice()
1621            && only.kind() == StatementKind::ImmediateAssertion
1622            && only.is_deferred_assertion() != Some(true)
1623        {
1624            out.push(name);
1625        }
1626        stack.extend(live_children(&n));
1627    }
1628    out
1629}
1630
1631/// Whether `stmt` is a labeled immediate assertion (`L: assert (c) begin
1632/// ... disable L; ... end`, a block named `L` holding only the assertion)
1633/// whose only `disable` targets `L` itself and which contains no other
1634/// suspend point. Disabling the label ends the assertion statement (LRM
1635/// 9.6.2: a labeled statement is a named scope), so the whole block is one
1636/// straight-line leaf for the CFG and codegen leaves it through an exit
1637/// block (`process_lower`'s `List | Block` arm).
1638pub fn self_disabling_assertion_block(stmt: &Statement<'_>) -> bool {
1639    if stmt.kind() != StatementKind::Block {
1640        return false;
1641    }
1642    let name = block_name(stmt);
1643    if name.is_empty() {
1644        return false;
1645    }
1646    let children = stmt.statements();
1647    let [only] = children.as_slice() else {
1648        return false;
1649    };
1650    if only.kind() != StatementKind::ImmediateAssertion {
1651        return false;
1652    }
1653    let mut disables = 0usize;
1654    let mut stack = vec![only.as_sem_node()];
1655    while let Some(n) = stack.pop() {
1656        if let Some(s) = n.as_statement()
1657            && s.kind() == StatementKind::Disable
1658        {
1659            match disable_label(&s) {
1660                Ok(l) if l == name => disables += 1,
1661                _ => return false,
1662            }
1663        }
1664        stack.extend(live_children(&n));
1665    }
1666    disables > 0
1667        && !contains_suspend_trigger_in(
1668            only.as_sem_node(),
1669            &mut std::collections::HashSet::new(),
1670            true,
1671            false,
1672        )
1673}
1674
1675/// The CFG of the plan that hosts the `expect` statement `stmt` for the
1676/// assertion engine (LRM 1800-2023 §16.17): `stmt` is an ordinary leaf of it,
1677/// not a wait -- the process that blocks on the `expect` is another plan, built
1678/// by [`build_cfg_clocked`].
1679pub fn build_cfg_hosted_assertion<'d>(
1680    stmt: &Statement<'d>,
1681    default_clocking: Option<Symbol<'d>>,
1682) -> Result<ProcessCfg<'d>, LoweringError> {
1683    let mut builder = CfgBuilder::new();
1684    builder.default_clocking = default_clocking;
1685    builder.expect_as_leaf = true;
1686    let entry = builder.alloc_block();
1687    let exit = builder.walk_stmt(*stmt, entry)?;
1688    builder.close(exit, Terminator::End { loop_back: None });
1689    Ok(ProcessCfg {
1690        blocks: builder.finish(),
1691        entry,
1692        body: *stmt,
1693        inlined_locals: Vec::new(),
1694        captures: Vec::new(),
1695        captured: Vec::new(),
1696        ref_bindings: Vec::new(),
1697        disable_names: Vec::new(),
1698        fork_branch_names: Vec::new(),
1699        disable_scopes: Vec::new(),
1700        disable_broadcast: Vec::new(),
1701        inherited: Vec::new(),
1702        capture_types: Vec::new(),
1703        recursion: Recursion::None,
1704        share_id: None,
1705    })
1706}
1707
1708/// Whether a call was written `super.m(...)` (LRM 8.15: not virtual).
1709pub type SuperDetector = fn(Expression<'_>) -> bool;
1710
1711thread_local! {
1712    /// The [`SuperDetector`]: recovering `super.` needs the design's syntax,
1713    /// which codegen holds (`vj_codegen::classes::call_is_explicit_super`);
1714    /// the run installs it.
1715    static EXPLICIT_SUPER: std::cell::Cell<Option<SuperDetector>> =
1716        const { std::cell::Cell::new(None) };
1717}
1718
1719/// Installs (or, with `None`, removes) the `super.`-call detector
1720/// [`CfgBuilder::inline_task_call`] uses for virtual class tasks.
1721pub fn set_explicit_super_detector(f: Option<SuperDetector>) {
1722    EXPLICIT_SUPER.with(|c| c.set(f));
1723}
1724
1725fn is_explicit_super(call: Expression<'_>) -> bool {
1726    EXPLICIT_SUPER.with(|c| c.get().is_some_and(|f| f(call)))
1727}
1728
1729/// The design's class templates a dynamic type can be (every `new()`-reachable
1730/// class and its ancestors, with their dense template ids), for dispatching a
1731/// virtual class task that can suspend: such a task is inlined, so a call
1732/// through a base handle becomes a switch over the overrides the possible
1733/// dynamic classes resolve it to.
1734pub struct VirtualTasks<'d> {
1735    pub classes: Vec<(u32, sv_lang::Type<'d>)>,
1736    /// Per entry of `classes`, its base chain (root first, itself last) and
1737    /// the chain's symbol keys, filled on first use: `implementations` runs
1738    /// once per virtual method per fixed-point round, and rebuilding the
1739    /// chain and every ancestor's hierarchical path each time dominated the
1740    /// compile of a deep class library (`import uvm_pkg::*`).
1741    chains: std::cell::OnceCell<Vec<ClassChain<'d>>>,
1742    /// Per task, whether its body reaches itself ([`task_reaches`] with the
1743    /// task as target): decided once per task, not per call site, because
1744    /// each query walks every task reachable through virtual dispatch (a
1745    /// UVM testbench compiled for over an hour re-walking `uvm_pkg`).
1746    self_reach: std::cell::RefCell<std::collections::HashMap<crate::symid::SymbolId, bool>>,
1747}
1748
1749/// One class's base chain and the keys of its members ([`VirtualTasks`]).
1750struct ClassChain<'d> {
1751    chain: Vec<sv_lang::Type<'d>>,
1752    keys: std::collections::HashSet<crate::symid::SymbolId>,
1753}
1754
1755thread_local! {
1756    /// Every virtual method one of whose implementations (in the classes a
1757    /// dynamic type can be) can suspend ([`set_suspending_virtuals`]).
1758    static SUSPENDING_VIRTUALS: std::cell::RefCell<std::collections::HashSet<crate::symid::SymbolId>> =
1759        std::cell::RefCell::new(std::collections::HashSet::new());
1760}
1761
1762fn is_suspending_virtual(sub: &Symbol<'_>) -> bool {
1763    SUSPENDING_VIRTUALS.with(|s| s.borrow().contains(&sub.uid()))
1764}
1765
1766/// Records which virtual methods can suspend through an override (LRM 8.20:
1767/// a call runs the dynamic class's implementation, so it suspends when any
1768/// implementation can): such a call is inlined as a dispatch over the
1769/// overrides. To a fixed point, since an implementation may suspend only
1770/// through another virtual call. Clears the previous design's record.
1771pub fn set_suspending_virtuals(vt: &VirtualTasks<'_>) {
1772    SUSPENDING_VIRTUALS.with(|s| s.borrow_mut().clear());
1773    let mut methods: Vec<(crate::symid::SymbolId, Symbol<'_>)> = Vec::new();
1774    for (_, ty) in &vt.classes {
1775        for m in ty.as_symbol().members() {
1776            let m = if m.kind() == SymbolKind::MethodPrototype {
1777                match m.method_prototype_subroutine() {
1778                    Some(real) => real,
1779                    None => continue,
1780                }
1781            } else {
1782                m
1783            };
1784            if m.kind() == SymbolKind::Subroutine
1785                && (m.subroutine_this_var().is_some()
1786                    || m.subroutine_flags().contains(sv_lang::MethodFlags::PURE))
1787                && (m.subroutine_flags().contains(sv_lang::MethodFlags::VIRTUAL)
1788                    || m.subroutine_override().is_some())
1789                && !methods.iter().any(|(_, x)| *x == m)
1790            {
1791                methods.push((m.uid(), m));
1792            }
1793        }
1794    }
1795    loop {
1796        let mut changed = false;
1797        for (uid, m) in &methods {
1798            if SUSPENDING_VIRTUALS.with(|s| s.borrow().contains(uid)) {
1799                continue;
1800            }
1801            if vt
1802                .implementations(m)
1803                .iter()
1804                .any(|(_, imp)| subroutine_can_suspend(imp))
1805            {
1806                SUSPENDING_VIRTUALS.with(|s| s.borrow_mut().insert(uid.clone()));
1807                changed = true;
1808            }
1809        }
1810        if !changed {
1811            break;
1812        }
1813    }
1814}
1815
1816impl<'d> VirtualTasks<'d> {
1817    /// The possible dynamic classes (template id, class type).
1818    pub fn new(classes: Vec<(u32, sv_lang::Type<'d>)>) -> Self {
1819        VirtualTasks {
1820            classes,
1821            chains: std::cell::OnceCell::new(),
1822            self_reach: std::cell::RefCell::new(std::collections::HashMap::new()),
1823        }
1824    }
1825
1826    /// The implementations of `method` the dynamic classes derived from (or
1827    /// equal to) its declaring class run, each with the template ids that
1828    /// run it: per class, the most-derived declaration of the name with a
1829    /// body.
1830    pub fn implementations(&self, method: &Symbol<'d>) -> Vec<(Vec<u32>, Symbol<'d>)> {
1831        let Some(decl) = method.parent() else {
1832            return Vec::new();
1833        };
1834        let name = method.name();
1835        let decl_key = decl.uid();
1836        let chains = self.chains.get_or_init(|| {
1837            self.classes
1838                .iter()
1839                .map(|(_, ty)| {
1840                    let mut chain = crate::netlist::base_chain(*ty);
1841                    chain.push(*ty);
1842                    let keys = chain.iter().map(|c| c.as_symbol().uid()).collect();
1843                    ClassChain { chain, keys }
1844                })
1845                .collect()
1846        });
1847        let mut out: Vec<(Vec<u32>, Symbol<'d>)> = Vec::new();
1848        for ((id, _), cc) in self.classes.iter().zip(chains) {
1849            if !cc.keys.contains(&decl_key) {
1850                continue;
1851            }
1852            let found = cc.chain.iter().rev().find_map(|c| {
1853                let s = c.as_symbol().find(name)?;
1854                let s = if s.kind() == SymbolKind::MethodPrototype {
1855                    s.method_prototype_subroutine()?
1856                } else {
1857                    s
1858                };
1859                (s.kind() == SymbolKind::Subroutine
1860                    && s.body().is_some()
1861                    && !s.subroutine_flags().contains(sv_lang::MethodFlags::PURE))
1862                .then_some(s)
1863            });
1864            let Some(imp) = found else { continue };
1865            match out.iter_mut().find(|(_, s)| *s == imp) {
1866                Some((ids, _)) => ids.push(*id),
1867                None => out.push((vec![*id], imp)),
1868            }
1869        }
1870        out
1871    }
1872}
1873
1874/// [`build_cfg`] for one branch of the fork block named `fork_name`, if
1875/// named: a `disable` of that name inside the branch ends the fork.
1876#[allow(clippy::too_many_arguments)]
1877fn build_cfg_in<'d>(
1878    body: &Statement<'d>,
1879    loop_kind: ProcessLoopKind,
1880    fork_name: Option<String>,
1881    default_clocking: Option<Symbol<'d>>,
1882    peers: Option<&[PeerBlock]>,
1883    checker_clock: Option<InferredClockEvent<'d>>,
1884    inherited: Inherited<'d>,
1885    virtual_tasks: Option<std::rc::Rc<VirtualTasks<'d>>>,
1886) -> Result<ProcessCfg<'d>, LoweringError> {
1887    // LRM 9.6.2: the name(s) a `disable` ANYWHERE may single this CFG's own
1888    // process out by -- computed before `fork_name` moves into `builder`.
1889    let mut disable_names: Vec<String> = Vec::new();
1890    disable_names.extend(fork_name.clone());
1891    disable_names.extend(peer_block(body).map(|p| p.name));
1892
1893    let mut builder = CfgBuilder::new();
1894    builder.this_stack.extend(inherited.this_var);
1895    builder.ref_stack = inherited.ref_aliases;
1896    builder.global_clock = inherited.global_clock;
1897    builder.detached_trigger = inherited.detached_trigger;
1898    builder.forced_extern = inherited.forced_extern;
1899    builder.rec_outer = inherited.rec_outer;
1900    if let Some(memo) = inherited.fork_memo {
1901        builder.fork_memo = memo;
1902    }
1903    if let Some(rec) = inherited.rec {
1904        // The same capture temporaries the activation reserves.
1905        for _ in 0..rec.groups {
1906            for &(a, f) in &rec.proto {
1907                builder.set_capture_type(a, f);
1908            }
1909        }
1910        builder.rec = Some(rec);
1911    }
1912    builder.rec_up = inherited.rec_up;
1913    builder.virtual_tasks = virtual_tasks;
1914    builder.fork_name = fork_name;
1915    builder.fork_branch_names = inherited.known_fork_labels;
1916    // LRM 9.6.2: a branch also answers to `disable` of any block enclosing
1917    // its fork (it is an activity enabled within that block), so a block that
1918    // ends with detached branches still running ends them. Only names some
1919    // `disable` of the design mentions are tagged.
1920    builder.disable_names_extra(&inherited.ancestor_blocks, &mut disable_names);
1921    builder.body_node = Some(body.as_sem_node());
1922    builder.ancestor_blocks = inherited.ancestor_blocks;
1923    builder.default_clocking = default_clocking;
1924    builder.checker_clock = checker_clock;
1925    if let Some(peers) = peers {
1926        // A fork branch is not the process's own scope: only a whole process
1927        // has an outermost scope, assertion labels and peers.
1928        builder.peers = peers.to_vec();
1929        builder.assertion_labels = assertion_labels(body)
1930            .into_iter()
1931            .map(|(name, _)| name)
1932            .collect();
1933        builder.deferred_labels = deferred_assertion_labels(body)
1934            .into_iter()
1935            .map(|(name, _)| name)
1936            .collect();
1937        builder.immediate_labels = immediate_assertion_labels(body);
1938        builder.outermost = peer_block(body).map(|p| p.name);
1939    }
1940    let entry = builder.alloc_block();
1941    let exit = builder.walk_stmt(*body, entry)?;
1942    let loop_back = match loop_kind {
1943        ProcessLoopKind::OneShot => None,
1944        ProcessLoopKind::Looping => Some(entry),
1945    };
1946    builder.close(exit, Terminator::End { loop_back });
1947    let ref_bindings = builder.take_ref_bindings();
1948    let inlined_locals = std::mem::take(&mut builder.inlined_locals);
1949    let captures = std::mem::take(&mut builder.captures);
1950    let capture_types = std::mem::take(&mut builder.capture_types);
1951    let fork_branch_names = std::mem::take(&mut builder.fork_branch_names);
1952    let disable_scopes = builder.take_disable_scopes();
1953    let disable_broadcast = std::mem::take(&mut builder.disable_broadcast);
1954    Ok(ProcessCfg {
1955        blocks: builder.finish(),
1956        entry,
1957        body: *body,
1958        inlined_locals,
1959        captures,
1960        captured: Vec::new(),
1961        ref_bindings,
1962        disable_names,
1963        fork_branch_names,
1964        disable_scopes,
1965        disable_broadcast,
1966        inherited: Vec::new(),
1967        capture_types,
1968        recursion: Recursion::None,
1969        share_id: None,
1970    })
1971}
1972
1973/// The recursive task whose activation a builder is walking.
1974#[derive(Clone)]
1975struct RecursiveTask<'d> {
1976    task: crate::symid::SymbolId,
1977    /// The [`ProcessCfg::share_id`] of this activation's Root (unique per
1978    /// distinct [`RootKey`]); part of the key of every Root nested in it.
1979    share: u64,
1980    /// The first call's actuals and the formals they shape (the capture
1981    /// temporaries' types, [`CfgBuilder::set_capture_type`]), so a fork
1982    /// branch inside the activation can reserve the same temporaries.
1983    proto: Vec<(Expression<'d>, Symbol<'d>)>,
1984    /// The number of formals: capture temporaries `0..n` hold the incoming
1985    /// arguments, `n..2n` the arguments of a recursive call.
1986    n: u32,
1987    /// The number of temporary groups of `n`: 2, or 4 when a formal is an
1988    /// `output`/`inout` (`2n..3n` the activation's results, `3n..4n` those
1989    /// of a recursive call).
1990    groups: u32,
1991    /// Per formal: whether it is an `output`/`inout`.
1992    outs: Vec<bool>,
1993    /// Per formal: the static storage an aliased `ref` formal names.
1994    refs: Vec<Option<Symbol<'d>>>,
1995    /// A class method: capture temporary 0 (formal 0) is its `this`.
1996    has_this: bool,
1997    body: Statement<'d>,
1998}
1999
2000/// Everything a fork branch's CFG is a function of besides its statement:
2001/// the key of [`CfgBuilder::fork_memo`]. A branch is walked by a fresh
2002/// builder seeded only from [`Inherited`], so equal keys give equal CFGs.
2003#[derive(PartialEq, Eq, Hash)]
2004struct ForkBranchKey {
2005    /// Address and kind of the branch statement's AST node.
2006    stmt: (usize, u16),
2007    fork_name: Option<String>,
2008    default_clocking: Option<crate::symid::SymbolId>,
2009    this_var: Option<crate::symid::SymbolId>,
2010    ref_aliases: Vec<(crate::symid::SymbolId, crate::symid::SymbolId)>,
2011    rec_outer: Vec<crate::symid::SymbolId>,
2012    labels: Vec<String>,
2013    ancestors: Vec<String>,
2014}
2015
2016/// Everything a [`Recursion::Root`] activation's CFG is a function of: the
2017/// key of its entry in [`CfgBuilder::fork_memo`]. A Root is walked by a fresh
2018/// builder seeded from exactly these fields (the call site's actuals only
2019/// supply per-site capture bindings; the capture temporaries are typed by
2020/// the formals), so equal keys give equal CFGs. `chain` is the
2021/// [`RecursiveTask::share`] ids of the enclosing activations, which pins
2022/// each one's CFG (and so its entry block and frame layout, which an
2023/// [`Recursion::AncestorSpawn`] inside this Root copies from).
2024#[derive(PartialEq, Eq, Hash)]
2025struct RootKey {
2026    task: crate::symid::SymbolId,
2027    this_var: Option<crate::symid::SymbolId>,
2028    default_clocking: Option<crate::symid::SymbolId>,
2029    rec_outer: Vec<crate::symid::SymbolId>,
2030    chain: Vec<u64>,
2031    ref_aliases: Vec<(crate::symid::SymbolId, crate::symid::SymbolId)>,
2032    labels: Vec<String>,
2033    ancestors: Vec<String>,
2034}
2035
2036/// A [`CfgBuilder::fork_memo`] key: a fork branch or a recursive-task Root.
2037#[derive(PartialEq, Eq, Hash)]
2038enum MemoKey {
2039    Fork(ForkBranchKey),
2040    Root(RootKey),
2041}
2042
2043/// The fork branches and recursive-task Roots of one process already built,
2044/// by [`MemoKey`].
2045type ForkMemo<'d> =
2046    std::rc::Rc<std::cell::RefCell<std::collections::HashMap<MemoKey, ProcessCfg<'d>>>>;
2047
2048/// Source of [`ProcessCfg::share_id`]s (unique across the program).
2049static NEXT_SHARE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
2050
2051/// The AST node address of a statement handle (a handle is a copy of the
2052/// FFI node pointer; two handles to one node compare equal here).
2053fn stmt_addr(s: &Statement<'_>) -> usize {
2054    // SAFETY: `Statement` wraps one `repr(C)` `slang_ast` whose first field
2055    // is the node pointer; reading that word reads the pointer.
2056    unsafe { *std::ptr::from_ref(s).cast::<usize>() }
2057}
2058
2059/// What a fork branch inherits from the inlined task it is forked in: its
2060/// `this` and its aliased `ref` formals.
2061#[derive(Default)]
2062struct Inherited<'d> {
2063    this_var: Option<Symbol<'d>>,
2064    ref_aliases: Vec<(Symbol<'d>, Symbol<'d>)>,
2065    /// [`CfgBuilder::global_clock`].
2066    global_clock: Option<InferredClockEvent<'d>>,
2067    /// [`CfgBuilder::detached_trigger`].
2068    detached_trigger: bool,
2069    /// [`CfgBuilder::forced_extern`].
2070    forced_extern: Option<Symbol<'d>>,
2071    /// [`CfgBuilder::rec_outer`].
2072    rec_outer: Vec<crate::symid::SymbolId>,
2073    /// [`CfgBuilder::fork_memo`], shared by a whole process's builders.
2074    fork_memo: Option<ForkMemo<'d>>,
2075    /// [`CfgBuilder::rec`]: a fork branch inside a recursive activation can
2076    /// itself call the task (each such call spawns the activation again).
2077    rec: Option<RecursiveTask<'d>>,
2078    /// [`CfgBuilder::rec_up`].
2079    rec_up: Vec<RecursiveTask<'d>>,
2080    /// [`CfgBuilder::fork_branch_names`]'s own starting point: every label
2081    /// already known reachable from here (the parent's own accumulated list
2082    /// at the fork site, plus this fork's sibling branches) before this
2083    /// CFG's own walk adds any it discovers deeper in its own body.
2084    known_fork_labels: Vec<String>,
2085    /// [`CfgBuilder::ancestor_blocks`]' starting point.
2086    ancestor_blocks: Vec<String>,
2087}
2088
2089/// A VPI frame probe of an inlined blocking task (37.43): its activation
2090/// begins (after its arguments and locals are set up) or ends.
2091#[derive(Clone, Debug)]
2092pub enum FrameProbe<'d> {
2093    Enter {
2094        task: Symbol<'d>,
2095        /// Its automatic variables: formals, then locals.
2096        vars: Vec<Symbol<'d>>,
2097        /// The `ref` formals the call bound by copy-in/copy-out (an actual
2098        /// that is not static storage) with their actuals, in formal order
2099        /// (37.15 detail 3). They are ref objs of the frame, not variables
2100        /// (so absent from `vars`).
2101        bound: Vec<(Symbol<'d>, Expression<'d>)>,
2102    },
2103    Exit {
2104        task: Symbol<'d>,
2105        vars: Vec<Symbol<'d>>,
2106    },
2107}
2108
2109struct PartialBlock<'d> {
2110    bindings: Vec<ArgBinding<'d>>,
2111    this_var: Option<Symbol<'d>>,
2112    ref_aliases: Vec<(Symbol<'d>, Symbol<'d>)>,
2113    stmts: Vec<Statement<'d>>,
2114    probes: Vec<(usize, Statement<'d>)>,
2115    disable_probes: Vec<(usize, Statement<'d>)>,
2116    frame_probes: Vec<(usize, FrameProbe<'d>)>,
2117    block_events: Vec<(usize, Symbol<'d>, bool)>,
2118    stmt_scopes: Vec<(usize, Symbol<'d>)>,
2119    step_exprs: Vec<Expression<'d>>,
2120    repeat_init: Option<(RepeatCounterId, RepeatCount<'d>)>,
2121    repeat_decrement: Option<RepeatCounterId>,
2122    expect_resume: bool,
2123    terminator: Option<Terminator<'d>>,
2124}
2125
2126struct CfgBuilder<'d> {
2127    blocks: Vec<PartialBlock<'d>>,
2128    /// Task 1: the single clock signal+edge `##N` (`CycleDelay`) is lowered
2129    /// against, inferred from the first `posedge`/`negedge` `SuspendPoint::
2130    /// Edge` this walk encounters (in practice, the enclosing `always_ff`'s
2131    /// own edge). `None` until seen; becomes permanently ambiguous
2132    /// (`Some(None)`) the moment a *second*, differently-referenced edge
2133    /// signal (or the SAME signal with a different edge -- Task 6 extends
2134    /// the ambiguity check to cover this too, since `CycleDelay` now carries
2135    /// a real `edge` field it must be unambiguous about) is seen, so a later
2136    /// `##N` inside a multi-clock or unclocked-context process is a named
2137    /// `LoweringError`, never a silent guess (`walk_timed`'s `CycleDelay`
2138    /// arm). Task 6: also threaded through as real `SuspendPoint::
2139    /// CycleDelay{clock, edge, ..}` payload -- previously computed
2140    /// then discarded once the ambiguity check passed.
2141    inferred_clock: Option<Option<(Expression<'d>, EdgeKind)>>,
2142    /// R-run Task 8: the next fresh [`RepeatCounterId`] this builder will
2143    /// mint -- dense, allocation-order-numbered, mirrors [`Self::blocks`]'s
2144    /// own `len()`-as-next-id discipline for [`BlockId`].
2145    next_repeat_counter: u32,
2146    /// Formals/locals of every task inlined so far (`ProcessCfg::
2147    /// inlined_locals`).
2148    inlined_locals: Vec<Symbol<'d>>,
2149    /// Every inlined call's `ref` formal with the symbol its actual names
2150    /// (`None` for an actual that is not a plain name): resolved into
2151    /// `ProcessCfg::ref_bindings` once the whole process is walked.
2152    ref_candidates: Vec<(Symbol<'d>, Option<Symbol<'d>>)>,
2153    /// Set while walking a block whose automatic variables were just
2154    /// bound on entry (so the block itself is walked normally).
2155    entering_block: bool,
2156    /// The call being inlined, whose type shapes the `this` capture of a
2157    /// recursive method call with an implicit receiver.
2158    call_anchor: Option<Expression<'d>>,
2159    /// The capture temporary the next [`Self::inline_task_call`] parks the
2160    /// called function's result in ([`ArgBinding::CallResult`]).
2161    pending_result_temp: Option<(u32, Expression<'d>)>,
2162    /// Where a `return;` inside the innermost task being inlined goes.
2163    return_targets: Vec<BlockId>,
2164    /// The result variable of each inlined function (`None` for a task or void
2165    /// function), parallel to the inlined entries of [`Self::return_targets`]:
2166    /// `return expr;` assigns it (LRM 1800-2023 13.4.1).
2167    ret_var_stack: Vec<Option<Symbol<'d>>>,
2168    /// The `this` of each class-method task being inlined, innermost last
2169    /// ([`BasicBlock::this_var`]).
2170    this_stack: Vec<Symbol<'d>>,
2171    /// The `ref` formals of the tasks being inlined that alias their call's
2172    /// storage ([`BasicBlock::ref_aliases`]), innermost last.
2173    ref_stack: Vec<(Symbol<'d>, Symbol<'d>)>,
2174    /// The class templates a virtual suspending task can dispatch over.
2175    virtual_tasks: Option<std::rc::Rc<VirtualTasks<'d>>>,
2176    /// The type source of each capture temporary ([`ProcessCfg::capture_types`]).
2177    capture_types: Vec<Option<Symbol<'d>>>,
2178    /// Tasks currently being inlined (recursion guard).
2179    inline_stack: Vec<crate::symid::SymbolId>,
2180    /// While building a recursive task's activation
2181    /// ([`Self::inline_recursive_task`]): the task, whose calls are forks.
2182    rec: Option<RecursiveTask<'d>>,
2183    /// The recursive activations enclosing [`Self::rec`]'s (outermost first):
2184    /// a call of one of their tasks is a spawn of that activation
2185    /// ([`Recursion::AncestorSpawn`]).
2186    rec_up: Vec<RecursiveTask<'d>>,
2187    /// The recursive tasks whose activation encloses this builder's CFG (a
2188    /// fork branch inside one cannot start another activation of it).
2189    rec_outer: Vec<crate::symid::SymbolId>,
2190    /// The innermost enclosing loop's `continue` and `break` targets.
2191    loop_targets: Vec<(BlockId, BlockId)>,
2192    /// The sequential blocks being walked that declare automatic variables
2193    /// holding class or collection handles, innermost last (LRM 6.21: such a
2194    /// variable ends with its block, and a handle is released then).
2195    owned_scopes: Vec<Vec<Symbol<'d>>>,
2196    /// For each jump target (`break`/`continue`/`return`/`disable` exit), how
2197    /// many [`Self::owned_scopes`] were open outside it: a jump to it
2198    /// releases the scopes above that depth ([`Self::jump`]).
2199    target_depth: std::collections::HashMap<BlockId, usize>,
2200    /// The enclosing named blocks a `disable` may leave: `(name, exit)`,
2201    /// innermost last.
2202    /// `(label, exit, frames)`: `frames` is how many inlined tasks were
2203    /// active (`frame_stack`) when the block was entered.
2204    disable_targets: Vec<(String, BlockId, usize)>,
2205    /// The body this builder walks, for the lazy [`Self::body_forks`] scan.
2206    body_node: Option<SemNode<'d>>,
2207    /// The names of the fork blocks this builder walked (a subset of
2208    /// [`Self::fork_branch_names`]): a `fl.b.x` path through one is an
2209    /// instance-path-style label, not a branch name.
2210    fork_block_names: Vec<String>,
2211    /// Whether the body (or a task it calls) holds a fork block: decided on
2212    /// the first `disable` that jumps out of a named block, since only then
2213    /// can detached branches need ending with it (LRM 9.6.2).
2214    body_forks: Option<bool>,
2215    /// The covergroup-event blocks and inlined tasks being walked, innermost
2216    /// last, each with `disable_targets.len()` when it was entered: a
2217    /// `disable` that jumps out of them ends them there (LRM 9.6.2, 19.3), so
2218    /// [`Self::left_block_events`] signals their `end` at the jump.
2219    event_stack: Vec<(Symbol<'d>, usize)>,
2220    /// The tasks being inlined, innermost last, with their automatic
2221    /// variables (for the VPI frame probes a `disable` jumping out of them
2222    /// owes).
2223    frame_stack: Vec<(Symbol<'d>, Vec<Symbol<'d>>)>,
2224    /// The named blocks and inlined task activations being walked whose name
2225    /// some `disable` of the design targets, `(label, exit)`, innermost last:
2226    /// every block allocated while one is open is inside it
2227    /// ([`Self::block_scopes`], [`ProcessCfg::disable_scopes`]).
2228    /// `frames` is how many inlined tasks were active ([`Self::frame_stack`])
2229    /// outside the scope.
2230    scope_chain: Vec<(String, BlockId, usize)>,
2231    /// The names of the named sequential blocks being walked, outermost first
2232    /// (the path `disable p.b.x;` names, LRM 9.6.2).
2233    name_path: Vec<String>,
2234    /// [`Self::scope_chain`] as it was when each block was allocated.
2235    block_scopes: Vec<Vec<(String, BlockId, usize)>>,
2236    /// [`Self::frame_stack`] as it was when each block was allocated (empty
2237    /// outside every scope).
2238    block_frames: Vec<Vec<(Symbol<'d>, Vec<Symbol<'d>>)>>,
2239    /// [`ProcessCfg::disable_broadcast`].
2240    disable_broadcast: Vec<(String, bool)>,
2241    /// When this CFG is one branch of a named fork block: that name, which
2242    /// a `disable` inside it may target (`sv_rt_disable_fork_block`).
2243    fork_name: Option<String>,
2244    /// Every fork-branch [`ProcessCfg::disable_names`] a `disable <label>;`
2245    /// written anywhere in THIS CFG's own remaining walk may legitimately
2246    /// target (LRM 9.6.2), besides a same-process lexically enclosing block
2247    /// or this CFG's own `fork_name`: seeded from [`Inherited::
2248    /// known_fork_labels`] (what the parent already knew, plus this fork's
2249    /// siblings, when this CFG is itself a branch), then extended, going
2250    /// forward, by every later fork THIS walk itself builds branches for --
2251    /// never popped (unlike `disable_targets`): LRM 9.6.2 lets a branch's
2252    /// name be disabled for the rest of the process's life, not only while
2253    /// the `fork` statement that spawned it is still being walked.
2254    fork_branch_names: Vec<String>,
2255    /// The named blocks enclosing the fork statement(s) this CFG is a branch of,
2256    /// in the forking process(es) (LRM 9.6.2): a `disable` of one from inside
2257    /// the branch ends that block's whole activity, the forking process
2258    /// suspended at the join included, which resumes after the block.
2259    ancestor_blocks: Vec<String>,
2260    /// The name of the process's outermost scope (LRM 16.14.6.4), if it is a
2261    /// named block: a `disable` of it flushes the pending procedural assertion
2262    /// queue.
2263    outermost: Option<String>,
2264    /// The labels of the procedural concurrent assertions of this process: a
2265    /// `disable <label>;` clears the pending instances of that assertion
2266    /// (16.14.6.4).
2267    assertion_labels: Vec<String>,
2268    /// Labels of the process's deferred immediate assertions: `disable L;`
2269    /// cancels their pending reports (16.4.4), codegen lowers the kept statement.
2270    deferred_labels: Vec<String>,
2271    /// Labels of the process's plain immediate assertions: `disable L;` from
2272    /// outside the labeled statement is a no-op (it already finished).
2273    immediate_labels: Vec<String>,
2274    /// The outermost named blocks of the instance's other processes, which this
2275    /// process may `disable` (16.14.6.4, the `b3` example).
2276    peers: Vec<PeerBlock>,
2277    /// [`ProcessCfg::captures`].
2278    captures: Vec<Expression<'d>>,
2279    /// The instance's default clocking block, which `##N` counts.
2280    default_clocking: Option<Symbol<'d>>,
2281    /// While lowering the timing control of a clocking drive (`cb.q <= ##N v`):
2282    /// the drive target's own clocking block, whose event `##N` counts (LRM
2283    /// 1800-2023 14.16) instead of the default clocking's.
2284    drive_clocking: Option<Symbol<'d>>,
2285    /// The clock variable (`v.sb.c`) of that drive when it goes through a
2286    /// virtual interface member access, else `None` (see
2287    /// [`SuspendPoint::CycleDelay`]'s `via`).
2288    drive_via: Option<Expression<'d>>,
2289    /// Set by [`Self::walk_timed`] around its [`Self::suspend_point`] call: a
2290    /// run-time `##(expr)` count is then accepted (as a single `##1` wait the
2291    /// caller wraps in a counted loop) and its expression left in
2292    /// `dynamic_cycle_count`; every other caller still refuses it by name.
2293    allow_dynamic_cycle: bool,
2294    /// Set by [`Self::walk_timed`] around the [`Self::suspend_point`] of a
2295    /// mixed class-event / signal `@(a or b)` list: the class terms are then
2296    /// skipped (the leaf statement queues the process on them) and only the
2297    /// signal terms form the suspend point. Every other caller keeps the class
2298    /// term, so it cannot be dropped silently.
2299    skip_class_event_terms: bool,
2300    dynamic_cycle_count: Option<Expression<'d>>,
2301    /// What a checker procedure's `@clock` waits on when `clock` is the default
2302    /// `$inferred_clock` ([`InferredClockEvent`]).
2303    checker_clock: Option<InferredClockEvent<'d>>,
2304    /// What `@$global_clock` waits on: the event of the global clocking in
2305    /// effect for the process (14.14), when one is declared in its instance.
2306    global_clock: Option<InferredClockEvent<'d>>,
2307    /// Walking the body of the plan that hosts an `expect` statement for the
2308    /// assertion engine ([`build_cfg_hosted_assertion`]): the `expect` is then
2309    /// an ordinary leaf, not a blocking wait.
2310    expect_as_leaf: bool,
2311    /// This CFG is the detached process of a `->> @(ctl) e;` (LRM 1800-2023
2312    /// 15.5.2): its body statement is that trigger, which waits on `ctl` and
2313    /// then stages the nonblocking trigger, while the issuing process
2314    /// continues. Cleared once the body statement is walked.
2315    detached_trigger: bool,
2316    /// The fork branches this process has built so far ([`ForkBranchKey`]):
2317    /// a task inlined at many call sites repeats the same forks, and each
2318    /// distinct branch is built (and later lowered) once.
2319    fork_memo: ForkMemo<'d>,
2320    /// This CFG is one branch of a call to an `extern forkjoin` task (LRM
2321    /// 25.7.5): its body statement is that call, and it runs this one
2322    /// implementation. Cleared once the body statement is walked.
2323    forced_extern: Option<Symbol<'d>>,
2324}
2325
2326impl<'d> CfgBuilder<'d> {
2327    fn new() -> Self {
2328        Self {
2329            expect_as_leaf: false,
2330            detached_trigger: false,
2331            fork_memo: ForkMemo::default(),
2332            forced_extern: None,
2333            blocks: Vec::new(),
2334            inferred_clock: None,
2335            next_repeat_counter: 0,
2336            inlined_locals: Vec::new(),
2337            ref_candidates: Vec::new(),
2338            entering_block: false,
2339            return_targets: Vec::new(),
2340            ret_var_stack: Vec::new(),
2341            this_stack: Vec::new(),
2342            ref_stack: Vec::new(),
2343            virtual_tasks: None,
2344            inline_stack: Vec::new(),
2345            capture_types: Vec::new(),
2346            rec: None,
2347            rec_up: Vec::new(),
2348            rec_outer: Vec::new(),
2349            loop_targets: Vec::new(),
2350            owned_scopes: Vec::new(),
2351            target_depth: std::collections::HashMap::new(),
2352            disable_targets: Vec::new(),
2353            body_node: None,
2354            fork_block_names: Vec::new(),
2355            body_forks: None,
2356            event_stack: Vec::new(),
2357            call_anchor: None,
2358            pending_result_temp: None,
2359            frame_stack: Vec::new(),
2360            scope_chain: Vec::new(),
2361            name_path: Vec::new(),
2362            block_scopes: Vec::new(),
2363            block_frames: Vec::new(),
2364            disable_broadcast: Vec::new(),
2365            ancestor_blocks: Vec::new(),
2366            fork_name: None,
2367            fork_branch_names: Vec::new(),
2368            outermost: None,
2369            assertion_labels: Vec::new(),
2370            deferred_labels: Vec::new(),
2371            immediate_labels: Vec::new(),
2372            peers: Vec::new(),
2373            captures: Vec::new(),
2374            default_clocking: None,
2375            drive_clocking: None,
2376            drive_via: None,
2377            allow_dynamic_cycle: false,
2378            skip_class_event_terms: false,
2379            dynamic_cycle_count: None,
2380            checker_clock: None,
2381            global_clock: None,
2382        }
2383    }
2384
2385    fn alloc_block(&mut self) -> BlockId {
2386        let id = BlockId(self.blocks.len() as u32);
2387        let this_var = self.this_stack.last().copied();
2388        let ref_aliases = self.ref_stack.clone();
2389        self.block_scopes.push(self.scope_chain.clone());
2390        self.block_frames.push(if self.scope_chain.is_empty() {
2391            Vec::new()
2392        } else {
2393            self.frame_stack.clone()
2394        });
2395        self.blocks.push(PartialBlock {
2396            bindings: Vec::new(),
2397            this_var,
2398            ref_aliases,
2399            stmts: Vec::new(),
2400            probes: Vec::new(),
2401            disable_probes: Vec::new(),
2402            frame_probes: Vec::new(),
2403            block_events: Vec::new(),
2404            stmt_scopes: Vec::new(),
2405            step_exprs: Vec::new(),
2406            repeat_init: None,
2407            repeat_decrement: None,
2408            expect_resume: false,
2409            terminator: None,
2410        });
2411        id
2412    }
2413
2414    /// After a statement that called a subroutine, in a design where some
2415    /// function has `disable <block>;` (LRM 9.6.2, 35.9): leaves the enclosing
2416    /// named block the call disabled, if any ([`SwitchOn::FnDisable`]).
2417    /// Returns the block the statements go on in.
2418    fn fn_disable_check(&mut self, cur: BlockId) -> BlockId {
2419        let targets: Vec<(usize, String, BlockId, usize)> = self
2420            .disable_targets
2421            .iter()
2422            .enumerate()
2423            .rev()
2424            .filter(|(_, (n, _, _))| is_function_disable_label(n))
2425            .map(|(pos, (n, exit, frames))| (pos, n.clone(), *exit, *frames))
2426            .collect();
2427        if targets.is_empty() {
2428            return cur;
2429        }
2430        let next = self.alloc_block();
2431        let mut arms = Vec::new();
2432        let mut labels = Vec::new();
2433        for (pos, label, exit, frames) in targets {
2434            let arm = self.alloc_block();
2435            // The inlined tasks the jump leaves end here, innermost first.
2436            let left: Vec<_> = self.frame_stack[frames..].iter().rev().cloned().collect();
2437            for (task, vars) in left {
2438                self.frame_probe(arm, FrameProbe::Exit { task, vars });
2439            }
2440            self.left_block_events(arm, pos);
2441            self.jump(arm, exit);
2442            arms.push(arm);
2443            labels.push(label);
2444        }
2445        self.close(
2446            cur,
2447            Terminator::Switch {
2448                on: SwitchOn::FnDisable { labels },
2449                arms,
2450                default_blk: next,
2451                report: None,
2452            },
2453        );
2454        next
2455    }
2456
2457    fn push(&mut self, blk: BlockId, stmt: Statement<'d>) {
2458        let scope = self.frame_stack.last().map(|f| f.0);
2459        let b = &mut self.blocks[blk.0 as usize];
2460        if let Some(scope) = scope {
2461            b.stmt_scopes.push((b.stmts.len(), scope));
2462        }
2463        b.stmts.push(stmt);
2464    }
2465
2466    /// Records that `stmt` is about to execute at the current end of block
2467    /// `blk`'s statements ([`BasicBlock::probes`]).
2468    fn probe(&mut self, blk: BlockId, stmt: Statement<'d>) {
2469        let b = &mut self.blocks[blk.0 as usize];
2470        let at = b.stmts.len();
2471        b.probes.push((at, stmt));
2472    }
2473
2474    /// Records that the `disable` statement `stmt`, turned into a jump, has
2475    /// taken effect at the current end of block `blk`'s statements
2476    /// ([`BasicBlock::disable_probes`]).
2477    fn disable_probe(&mut self, blk: BlockId, stmt: Statement<'d>) {
2478        let b = &mut self.blocks[blk.0 as usize];
2479        let at = b.stmts.len();
2480        b.disable_probes.push((at, stmt));
2481    }
2482
2483    /// Appends to `out` the `blocks` some `disable` of the design names (a
2484    /// fork branch's extra [`ProcessCfg::disable_names`]).
2485    fn disable_names_extra(&self, blocks: &[String], out: &mut Vec<String>) {
2486        for b in blocks {
2487            if is_disabled_somewhere(b) && !out.contains(b) {
2488                out.push(b.clone());
2489            }
2490        }
2491    }
2492
2493    /// Whether the body being walked holds a fork block, in itself or in a
2494    /// task it calls (cached).
2495    fn forks_in_body(&mut self) -> bool {
2496        if let Some(v) = self.body_forks {
2497            return v;
2498        }
2499        let v = self.body_node.is_some_and(contains_fork_through_calls);
2500        self.body_forks = Some(v);
2501        v
2502    }
2503
2504    /// `disable a.b;` (LRM 9.6.2) whose segments are a contiguous chain of
2505    /// this process's lexically enclosing named blocks: a jump to the last
2506    /// one's exit, exactly like `disable b;`. Any other hierarchical target
2507    /// (an instance path, a block not enclosing the statement, or the
2508    /// outermost block / a task frame, whose lowering needs the plain label)
2509    /// stays loud.
2510    fn walk_hier_disable_of_enclosing(
2511        &mut self,
2512        stmt: Statement<'d>,
2513        cur: BlockId,
2514        path: &[String],
2515    ) -> Result<Option<BlockId>, LoweringError> {
2516        let loud = || {
2517            LoweringError::unsupported(format!(
2518                "disable of `{}` (a hierarchical name is supported only as a chain of the \
2519                 statement's own lexically enclosing named blocks, not the process's \
2520                 outermost block or a task frame; IEEE 1800-2023 9.6.2)",
2521                path.join(".")
2522            ))
2523        };
2524        let last = path.last().ok_or_else(loud)?;
2525        let Some(pos) = self.disable_targets.iter().rposition(|(n, _, _)| n == last) else {
2526            return Ok(None);
2527        };
2528        let chain_ok = pos + 1 >= path.len()
2529            && path
2530                .iter()
2531                .enumerate()
2532                .all(|(i, seg)| self.disable_targets[pos + 1 - path.len() + i].0 == *seg);
2533        if !chain_ok {
2534            return Ok(None);
2535        }
2536        let (_, exit, frames) = self.disable_targets[pos].clone();
2537        let outermost = pos == 0 && self.outermost.as_deref() == Some(last.as_str());
2538        if outermost || frames > 0 {
2539            return Err(loud());
2540        }
2541        self.disable_probe(cur, stmt);
2542        self.left_block_events(cur, pos);
2543        self.jump(cur, exit);
2544        Ok(Some(self.alloc_block()))
2545    }
2546
2547    /// The target of `disable a.b.c;` (LRM 9.6.2) when every segment names a
2548    /// block of ANOTHER process of this instance ([`PeerBlock`]): the last
2549    /// segment, handled like the plain `disable c;` of that peer. An instance
2550    /// path (or any segment that is not a peer block) stays loud, since the
2551    /// runtime resolves a label within the requester's own instance only.
2552    fn hier_peer_leaf(&self, path: &[String]) -> Result<String, LoweringError> {
2553        if path
2554            .iter()
2555            .all(|seg| self.peers.iter().any(|p| &p.name == seg))
2556        {
2557            return Ok(path[path.len() - 1].clone());
2558        }
2559        // `disable outer.inner;` / `disable top.outer.inner;` where `inner` is
2560        // a fork branch's own named block (a sibling or a later branch of this
2561        // process) or a block enclosing the fork, reached through blocks of
2562        // this process family (LRM 9.6.2): the same target as the plain
2563        // `disable inner;`. A leading segment that names none of them is the
2564        // instance's own name (only when a block segment follows it). Left as
2565        // the dotted text it would name a block of a child instance and match
2566        // nothing, a silent no-op.
2567        let known = |s: &String| {
2568            self.ancestor_blocks.contains(s)
2569                || self.fork_branch_names.contains(s)
2570                || self.disable_targets.iter().any(|(n, _, _)| n == s)
2571                || self.outermost.as_deref() == Some(s.as_str())
2572        };
2573        if let Some((leaf, mid)) = path.split_last()
2574            && known(leaf)
2575        {
2576            let skip = usize::from(mid.len() >= 2 && !known(&mid[0]));
2577            if mid[skip..].iter().all(known) {
2578                return Ok(leaf.clone());
2579            }
2580        }
2581        // `disable u1.blk;` (an instance path): the first segment names
2582        // neither a block of this or another process nor a task, so it is a
2583        // child instance. Kept as the dotted text; the runtime resolves it from
2584        // the requester's instance (the extra scope keys run.rs registers).
2585        let first = &path[0];
2586        if !self.peers.iter().any(|p| &p.name == first)
2587            && !self.disable_targets.iter().any(|(n, _, _)| n == first)
2588            && !is_task_name(first)
2589            && self.fork_name.as_deref() != Some(first.as_str())
2590            && !self
2591                .fork_branch_names
2592                .iter()
2593                .any(|n| n == first && !self.fork_block_names.contains(n))
2594        {
2595            return Ok(path.join("."));
2596        }
2597        // `disable p.b.x;` where `x` also names a block elsewhere: the dotted
2598        // path from another process's outermost block `p`, which the runtime
2599        // finds under the path-qualified scope label (`CfgBuilder::name_path`).
2600        if self.peers.iter().any(|p| &p.name == first) {
2601            return Ok(path.join("."));
2602        }
2603        Err(LoweringError::unsupported(format!(
2604            "disable of `{}` (a hierarchical name is supported only as a chain of the \
2605             statement's own lexically enclosing named blocks or of the named blocks of \
2606             another process of the same instance, not an instance path, the process's \
2607             outermost block or a task frame; IEEE 1800-2023 9.6.2)",
2608            path.join(".")
2609        )))
2610    }
2611
2612    /// Records the frame probe `probe` at the current end of block `blk`'s
2613    /// statements ([`BasicBlock::frame_probes`]).
2614    fn frame_probe(&mut self, blk: BlockId, probe: FrameProbe<'d>) {
2615        let b = &mut self.blocks[blk.0 as usize];
2616        let at = b.stmts.len();
2617        b.frame_probes.push((at, probe));
2618    }
2619
2620    /// Records the covergroup block event of `task` (LRM 19.3) at the current
2621    /// end of block `blk`'s statements ([`BasicBlock::block_events`]).
2622    fn block_event(&mut self, blk: BlockId, task: Symbol<'d>, is_end: bool) {
2623        let b = &mut self.blocks[blk.0 as usize];
2624        let at = b.stmts.len();
2625        b.block_events.push((at, task, is_end));
2626    }
2627
2628    /// A `disable` jumping to `disable_targets[pos]` ends every block and
2629    /// inlined task entered inside that target (LRM 9.6.2): records their
2630    /// `end` covergroup events (LRM 19.3) at the jump, innermost first. The
2631    /// target's own end event is the one at its exit block.
2632    fn left_block_events(&mut self, cur: BlockId, pos: usize) {
2633        let left: Vec<_> = self
2634            .event_stack
2635            .iter()
2636            .rev()
2637            .take_while(|(_, depth)| *depth > pos)
2638            .map(|(s, _)| *s)
2639            .collect();
2640        for s in left {
2641            self.block_event(cur, s, true);
2642        }
2643    }
2644
2645    fn push_step_expr(&mut self, blk: BlockId, expr: Expression<'d>) {
2646        self.blocks[blk.0 as usize].step_exprs.push(expr);
2647    }
2648
2649    /// R-run Task 8: mints a fresh, process-unique [`RepeatCounterId`] for a
2650    /// suspend-containing `repeat(N)` loop's own synthetic counter.
2651    fn alloc_repeat_counter(&mut self) -> RepeatCounterId {
2652        let id = RepeatCounterId(self.next_repeat_counter);
2653        self.next_repeat_counter += 1;
2654        id
2655    }
2656
2657    /// R-run Task 8: records `blk`'s own `repeat_init` -- see
2658    /// [`BasicBlock::repeat_init`]'s doc comment. `blk` must not already
2659    /// carry one (a builder-internal consistency check, like [`Self::close`]'s
2660    /// own "not already closed" invariant: every call site is
2661    /// `walk_repeat_loop`'s own `cur`, which allocates/owns exactly one
2662    /// `repeat_init` per call).
2663    fn set_repeat_init(&mut self, blk: BlockId, counter: RepeatCounterId, count: RepeatCount<'d>) {
2664        let slot = &mut self.blocks[blk.0 as usize].repeat_init;
2665        debug_assert!(
2666            slot.is_none(),
2667            "cfg builder tried to set block {blk:?}'s repeat_init twice"
2668        );
2669        *slot = Some((counter, count));
2670    }
2671
2672    /// R-run Task 8: records `blk`'s own `repeat_decrement` -- see
2673    /// [`BasicBlock::repeat_decrement`]'s doc comment.
2674    fn set_repeat_decrement(&mut self, blk: BlockId, counter: RepeatCounterId) {
2675        let slot = &mut self.blocks[blk.0 as usize].repeat_decrement;
2676        debug_assert!(
2677            slot.is_none(),
2678            "cfg builder tried to set block {blk:?}'s repeat_decrement twice"
2679        );
2680        *slot = Some(counter);
2681    }
2682
2683    /// Records `signal`/`edge` (from a just-built `SuspendPoint::Edge`) as
2684    /// the process's inferred single clock -- see `inferred_clock`'s own doc
2685    /// comment. Compares *by referenced symbol identity*, since two
2686    /// `Expression` handles for the same `clk` are not otherwise comparable;
2687    /// an edge with no resolvable symbol (e.g. a bit-select or expression,
2688    /// not a plain name) can never disambiguate a later `##N`, so it does
2689    /// not overwrite an already-known clock but also never establishes one
2690    /// on its own. Task 6: a second sighting of the SAME signal with a
2691    /// DIFFERENT edge also becomes ambiguous now (`CycleDelay` carries a
2692    /// real `edge` field it must be unambiguous about, unlike the
2693    /// pre-Task-6 bare cycle count).
2694    fn note_edge_signal(&mut self, signal: Expression<'d>, edge: EdgeKind) {
2695        let this_sym = signal.referenced_symbol().map(|s| s.uid());
2696        match &self.inferred_clock {
2697            None => {
2698                self.inferred_clock = Some(this_sym.is_some().then_some((signal, edge)));
2699            }
2700            Some(Some((existing, existing_edge))) => {
2701                let existing_sym = existing.referenced_symbol().map(|s| s.uid());
2702                if existing_sym.is_none() || this_sym != existing_sym || *existing_edge != edge {
2703                    self.inferred_clock = Some(None); // now ambiguous
2704                }
2705            }
2706            Some(None) => {} // already ambiguous; stays ambiguous
2707        }
2708    }
2709
2710    /// Closes `blk` with `term`. `blk` must not already be closed -- every
2711    /// call site in this file only ever passes a `BlockId` it just allocated
2712    /// or one a nested `walk_stmt` call returned (which is itself always
2713    /// still-open by the same invariant), so this is a builder-internal
2714    /// consistency check, not a condition reachable from malformed SV source.
2715    fn close(&mut self, blk: BlockId, term: Terminator<'d>) {
2716        let slot = &mut self.blocks[blk.0 as usize].terminator;
2717        debug_assert!(
2718            slot.is_none(),
2719            "cfg builder tried to close block {blk:?} twice"
2720        );
2721        *slot = Some(term);
2722    }
2723
2724    /// Closes `cur` with a jump to `target`, releasing on the way the handles
2725    /// of the block-scoped automatic variables the jump leaves (a `break`,
2726    /// `continue`, `return` or `disable` out of a block, LRM 6.21).
2727    fn jump(&mut self, cur: BlockId, target: BlockId) {
2728        let open = self.owned_scopes.len();
2729        let depth = self.target_depth.get(&target).copied().unwrap_or(open);
2730        let vars: Vec<Symbol<'d>> = self.owned_scopes[depth.min(open)..]
2731            .iter()
2732            .rev()
2733            .flat_map(|s| s.iter().rev().copied())
2734            .collect();
2735        if vars.is_empty() {
2736            self.close(cur, Terminator::Fallthrough(target));
2737            return;
2738        }
2739        let leave = self.alloc_block();
2740        self.close(cur, Terminator::Fallthrough(leave));
2741        for var in vars {
2742            self.blocks[leave.0 as usize]
2743                .bindings
2744                .push(ArgBinding::Release { var });
2745        }
2746        self.close(leave, Terminator::Fallthrough(target));
2747    }
2748
2749    /// The static variable or net a `ref` actual names: itself, or what an
2750    /// enclosing inlined call's aliased `ref` formal it names stands for.
2751    fn ref_storage(&self, actual: Expression<'d>) -> Option<Symbol<'d>> {
2752        if !crate::symid::is_value_name(actual.kind()) {
2753            return None;
2754        }
2755        let sym = actual.referenced_symbol()?;
2756        if sym.kind() == SymbolKind::Net
2757            || (sym.kind() == SymbolKind::Variable
2758                && sym.variable_lifetime() == sv_lang::VariableLifetime::Static)
2759        {
2760            return Some(sym);
2761        }
2762        self.ref_stack
2763            .iter()
2764            .rev()
2765            .find(|(f, _)| f.uid() == sym.uid())
2766            .map(|&(_, a)| a)
2767    }
2768
2769    /// Resolves [`Self::ref_candidates`] into `ProcessCfg::ref_bindings`: a
2770    /// `ref` formal every call binds to one static variable or net (directly,
2771    /// or through an enclosing inlined task's `ref` formal that is itself
2772    /// bound) names that storage, so its copy-in/copy-out bindings and its
2773    /// frame local are dropped. A formal bound to different actuals, or to
2774    /// an automatic variable or a select, keeps the copy semantics.
2775    fn take_ref_bindings(&mut self) -> Vec<(Symbol<'d>, Symbol<'d>)> {
2776        use std::collections::HashMap;
2777        let storage = |s: &Symbol<'d>| {
2778            s.kind() == SymbolKind::Net
2779                || (s.kind() == SymbolKind::Variable
2780                    && s.variable_lifetime() == sv_lang::VariableLifetime::Static)
2781        };
2782        // formal uid -> (formal, the one target symbol every call names), or
2783        // None once two calls disagree or one names no plain symbol.
2784        let mut targets: HashMap<crate::symid::SymbolId, (Symbol<'d>, Option<Symbol<'d>>)> =
2785            HashMap::new();
2786        let mut order = Vec::new();
2787        for (formal, named) in &self.ref_candidates {
2788            match targets.get_mut(&formal.uid()) {
2789                None => {
2790                    order.push(formal.uid());
2791                    targets.insert(formal.uid(), (*formal, *named));
2792                }
2793                Some((_, cur)) => {
2794                    if cur.map(|c| c.uid()) != named.map(|n| n.uid()) {
2795                        *cur = None;
2796                    }
2797                }
2798            }
2799        }
2800        // Follow a chain through enclosing tasks' bound `ref` formals.
2801        let resolve = |start: Symbol<'d>| -> Option<Symbol<'d>> {
2802            let mut cur = start;
2803            for _ in 0..=targets.len() {
2804                if storage(&cur) {
2805                    return Some(cur);
2806                }
2807                cur = targets.get(&cur.uid())?.1?;
2808            }
2809            None
2810        };
2811        let mut out = Vec::new();
2812        for uid in order {
2813            let (formal, named) = targets[&uid];
2814            if let Some(actual) = named.and_then(resolve) {
2815                out.push((formal, actual));
2816            }
2817        }
2818        let bound: std::collections::HashSet<crate::symid::SymbolId> =
2819            out.iter().map(|(f, _)| f.uid()).collect();
2820        for b in &mut self.blocks {
2821            b.bindings.retain(|x| match x {
2822                ArgBinding::In { formal, .. } | ArgBinding::Out { formal, .. } => {
2823                    !bound.contains(&formal.uid())
2824                }
2825                _ => true,
2826            });
2827        }
2828        self.inlined_locals.retain(|l| !bound.contains(&l.uid()));
2829        out
2830    }
2831
2832    /// [`ProcessCfg::disable_scopes`] from the scopes each block was allocated in.
2833    ///
2834    /// A resume point inside inlined tasks the disabled scope does not contain
2835    /// goes through a block of its own that ends their VPI frames first,
2836    /// innermost first (37.43), like a `disable` jump inside the process does.
2837    fn take_disable_scopes(&mut self) -> Vec<(BlockId, String, BlockId)> {
2838        let scopes = std::mem::take(&mut self.block_scopes);
2839        let frames = std::mem::take(&mut self.block_frames);
2840        let mut out = Vec::new();
2841        for (i, (chain, active)) in scopes.into_iter().zip(frames).enumerate() {
2842            for (label, exit, outside) in chain {
2843                let target = if active.len() > outside {
2844                    let leave = self.alloc_block();
2845                    for (task, vars) in active[outside..].iter().rev().cloned() {
2846                        self.frame_probe(leave, FrameProbe::Exit { task, vars });
2847                    }
2848                    self.close(leave, Terminator::Fallthrough(exit));
2849                    leave
2850                } else {
2851                    exit
2852                };
2853                out.push((BlockId(i as u32), label, target));
2854            }
2855        }
2856        out
2857    }
2858
2859    fn finish(self) -> Vec<BasicBlock<'d>> {
2860        self.blocks
2861            .into_iter()
2862            .map(|b| BasicBlock {
2863                bindings: b.bindings,
2864                this_var: b.this_var,
2865                ref_aliases: b.ref_aliases,
2866                stmts: b.stmts,
2867                probes: b.probes,
2868                disable_probes: b.disable_probes,
2869                frame_probes: b.frame_probes,
2870                block_events: b.block_events,
2871                stmt_scopes: b.stmt_scopes,
2872                step_exprs: b.step_exprs,
2873                repeat_init: b.repeat_init.clone(),
2874                repeat_decrement: b.repeat_decrement,
2875                expect_resume: b.expect_resume,
2876                terminator: b.terminator.expect(
2877                    "internal invariant: build_cfg must close every block it allocates \
2878                     before returning Ok",
2879                ),
2880            })
2881            .collect()
2882    }
2883
2884    /// PR-007's single recursive-descent walk. Returns the [`BlockId`] that
2885    /// is "current" for whatever statement follows `stmt` in its enclosing
2886    /// sequence. Every construct reachable in the A2 slice's restricted
2887    /// grammar falls through -- there is no `return`/`disable`/`break`/
2888    /// `continue` meaningfully reachable from a process body here (loops,
2889    /// functions and tasks are out of slice scope), so this never needs to
2890    /// signal "does not fall through".
2891    ///
2892    /// `sv_lang::kinds::StatementKind` is `#[non_exhaustive]` upstream (a
2893    /// `sv-lang-kinds` version bump can add a variant without breaking this
2894    /// crate's build), so a literal Rust `match` over it cannot itself be
2895    /// exhaustive-without-a-wildcard -- the compiler mandates a catch-all
2896    /// arm. Per this workspace's already-established pattern for these
2897    /// generated kind enums (`vj-codegen::module`'s `TimingControlKind`/
2898    /// `StatementKind` handling, `determinism::kind_guard`'s doc comment),
2899    /// this is instead an `if`/`else-if` chain that names every one of
2900    /// today's 32 variants explicitly (`StatementKind::COUNT`, pinned by
2901    /// `determinism::kind_guard::statement_kind_count_matches_pinned_sv_lang_
2902    /// version`), falling through to a named [`LoweringError`] -- never a
2903    /// silent no-op -- only if a future version bump adds a 33rd variant
2904    /// this file has not been reviewed against (which that pinned-count test
2905    /// catches first).
2906    fn walk_stmt(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
2907        // A statically dead `if`/`case` arm slang could not bind demotes the
2908        // enclosing statements to `Invalid` wrappers; the real statement is
2909        // recoverable (see [`unwrap_invalid`]).
2910        let stmt = unwrap_invalid(stmt);
2911        let kind = stmt.kind();
2912
2913        if kind == StatementKind::Invalid {
2914            return Err(LoweringError::malformed(
2915                "Invalid statement node -- slang rejected this statement; an error inside a \
2916                 statically dead `if`/`case` arm (e.g. a reversed range select `v[7:8]` of a \
2917                 descending vector under a false parameter condition) is suppressed by slang \
2918                 but still invalidates the whole enclosing procedure",
2919            ));
2920        }
2921        if kind == StatementKind::List {
2922            return self.walk_seq(live_statements(&stmt), cur);
2923        }
2924        // VPI `cbStmt` (38.36.1, Table 38-6): the statement is about to
2925        // execute. The detached process of `->> @(ctl) e;` runs the same
2926        // statement the issuer already probed. Loops are probed per iteration
2927        // by their own walkers (or by codegen when they stay a leaf).
2928        if is_entry_probe_kind(&stmt) && !self.detached_trigger {
2929            self.probe(cur, stmt);
2930        }
2931        if kind == StatementKind::Block {
2932            return self.walk_block(stmt, cur);
2933        }
2934        if kind == StatementKind::Conditional {
2935            return self.walk_conditional(stmt, cur);
2936        }
2937        if kind == StatementKind::Timed {
2938            return self.walk_timed(stmt, cur);
2939        }
2940        if kind == StatementKind::Wait {
2941            return self.walk_wait(stmt, cur);
2942        }
2943        if kind == StatementKind::WaitFork {
2944            // SC-125: a real suspend -- the scheduler resumes the process once
2945            // every child it forked has finished (`Suspend::WaitFork`).
2946            let resume = self.alloc_block();
2947            self.close(
2948                cur,
2949                Terminator::Suspend {
2950                    point: SuspendPoint::WaitFork,
2951                    resume_blk: resume,
2952                },
2953            );
2954            return Ok(resume);
2955        }
2956        if kind == StatementKind::ExpressionStatement && is_process_by_request_stmt(&stmt) {
2957            // Classes plan Task 13: `p.await();` / `p.suspend();`. The call
2958            // (an ordinary leaf statement) queues the request; the suspend
2959            // after it lets the scheduler apply it -- block in `await`,
2960            // park a self-suspend -- before the next statement runs.
2961            self.push(cur, stmt);
2962            let resume = self.alloc_block();
2963            self.close(
2964                cur,
2965                Terminator::Suspend {
2966                    point: SuspendPoint::ByRequest,
2967                    resume_blk: resume,
2968                },
2969            );
2970            // Classes plan Task 18: a mailbox `get`/`peek` delivers its message
2971            // into a variable, which a process that blocked cannot have done
2972            // when the call returned. The statement runs a second time after
2973            // the resume; the runtime tells the two phases apart (the first one
2974            // of a call that did not block leaves a marker, so the second is a
2975            // no-op: `vj_sim::sched::mailbox`).
2976            if is_mailbox_two_phase_stmt(&stmt) {
2977                self.push(resume, stmt);
2978            }
2979            return Ok(resume);
2980        }
2981        if kind == StatementKind::WaitOrder {
2982            return self.walk_wait_order(stmt, cur);
2983        }
2984        if is_expect(&stmt) && !self.expect_as_leaf {
2985            return self.walk_expect(stmt, cur);
2986        }
2987        if kind == StatementKind::RandCase
2988            && (contains_suspend_trigger(stmt.as_sem_node())
2989                || escapes(
2990                    stmt.as_sem_node(),
2991                    !self.loop_targets.is_empty(),
2992                    !self.return_targets.is_empty(),
2993                ))
2994        {
2995            return self.walk_randcase(stmt, cur);
2996        }
2997        if kind == StatementKind::EventTrigger {
2998            return self.walk_event_trigger(stmt, cur);
2999        }
3000        if kind == StatementKind::ExpressionStatement && async_pla_call(&stmt).is_some() {
3001            return self.walk_async_pla(stmt, cur);
3002        }
3003        if kind == StatementKind::DisableFork {
3004            // SC-125: never suspends -- an ordinary straight-line statement
3005            // that codegen lowers to a `sv_rt_disable_fork` call.
3006            self.push(cur, stmt);
3007            return Ok(cur);
3008        }
3009        if kind == StatementKind::Disable {
3010            // `disable blk;` inside `blk`: leave the block, like `break`
3011            // leaves a loop (LRM 1800-2023 §9.6.2). The target is read from
3012            // the statement's source text (`disable_label`).
3013            // `disable outer.inner;` (9.6.2 hierarchical block name) naming a
3014            // chain of this process's own lexically enclosing blocks is the
3015            // same jump as `disable inner;`.
3016            let mut label_override = None;
3017            if let Some(path) = disable_path(&stmt) {
3018                if let Some(next) = self.walk_hier_disable_of_enclosing(stmt, cur, &path)? {
3019                    return Ok(next);
3020                }
3021                // A block nested in another process's scope, named through
3022                // that scope's blocks (`disable p.inner;`).
3023                label_override = Some(self.hier_peer_leaf(&path)?);
3024            }
3025            let label = match label_override {
3026                Some(l) => l,
3027                None => disable_label(&stmt)?,
3028            };
3029            // `disable f;` inside a branch of fork `f`: ends every branch
3030            // (codegen's runtime call), this one included.
3031            if !self.disable_targets.iter().any(|(n, _, _)| *n == label)
3032                && self.fork_name.as_deref() == Some(label.as_str())
3033            {
3034                self.push(cur, stmt);
3035                self.close(cur, Terminator::End { loop_back: None });
3036                return Ok(self.alloc_block());
3037            }
3038            if let Some(pos) = self
3039                .disable_targets
3040                .iter()
3041                .rposition(|(n, _, _)| *n == label)
3042            {
3043                let (_, exit, frames) = self.disable_targets[pos].clone();
3044                // 16.14.6.4: disabling the process's outermost scope also
3045                // flushes its pending procedural assertion queue; a nested
3046                // block does not. Codegen turns the kept statement into the
3047                // flush.
3048                let flush = pos == 0 && self.outermost.as_deref() == Some(label.as_str());
3049                // A task activation, or a block inside one, can be live in
3050                // other processes too (LRM 9.6.2: every activation ends): the
3051                // kept statement also reaches them at run time.
3052                let broadcast = frames > 0;
3053                // LRM 9.6.2: leaving the block ends the activities enabled
3054                // within it, the fork branches still running among them
3055                // (tagged with the block's name, `disable_names`).
3056                let ends_branches =
3057                    !broadcast && self.forks_in_body() && is_disabled_somewhere(&label);
3058                if flush || broadcast || ends_branches {
3059                    self.push(cur, stmt);
3060                }
3061                if broadcast || ends_branches {
3062                    self.disable_broadcast.push((label.clone(), false));
3063                }
3064                self.disable_probe(cur, stmt);
3065                // The inlined tasks the jump leaves end here, innermost first.
3066                let left: Vec<_> = self.frame_stack[frames..].iter().rev().cloned().collect();
3067                for (task, vars) in left {
3068                    self.frame_probe(cur, FrameProbe::Exit { task, vars });
3069                }
3070                self.left_block_events(cur, pos);
3071                self.jump(cur, exit);
3072                return Ok(self.alloc_block());
3073            }
3074            // `disable a1;` of a procedural concurrent assertion's label
3075            // (16.14.6.4) clears that assertion's pending instances and goes
3076            // on: the statement is kept for codegen.
3077            if self.assertion_labels.contains(&label) {
3078                self.push(cur, stmt);
3079                return Ok(cur);
3080            }
3081            // `disable a1;` of a deferred immediate assertion's label (16.4.4):
3082            // cancels its pending reports and goes on; kept for codegen.
3083            if self.deferred_labels.contains(&label) {
3084                self.push(cur, stmt);
3085                return Ok(cur);
3086            }
3087            // `disable L;` of a plain immediate assertion's label from outside
3088            // its statement: the statement has finished, nothing to end.
3089            if self.immediate_labels.contains(&label) {
3090                return Ok(cur);
3091            }
3092            // `disable b2;` of another process's outermost block (16.14.6.4,
3093            // the `b3` example): the other process cannot be inside a block
3094            // without a suspend point, so the disable's whole effect is that
3095            // process's queue flush.
3096            if let Some(can_suspend) = self
3097                .peers
3098                .iter()
3099                .find(|p| p.name == label)
3100                .map(|p| p.can_suspend)
3101            {
3102                self.push(cur, stmt);
3103                // A block with a suspend point may have its process inside it:
3104                // the disable then resumes that process after the block
3105                // (`sv_rt_disable_named`, `ProcessCfg::disable_scopes`).
3106                if can_suspend {
3107                    self.disable_broadcast.push((label, true));
3108                }
3109                return Ok(cur);
3110            }
3111            // `disable u1.blk;` of a block of a child instance: every process
3112            // of that instance suspended inside the block resumes after it.
3113            if label.contains('.') {
3114                self.push(cur, stmt);
3115                self.disable_broadcast.push((label, true));
3116                return Ok(cur);
3117            }
3118            // `disable t;` of a task of the design that this process is not in:
3119            // it ends every activation of `t` in the other processes (LRM
3120            // 9.6.2) and the process goes on.
3121            if is_task_name(&label) {
3122                self.push(cur, stmt);
3123                self.disable_broadcast.push((label, true));
3124                return Ok(cur);
3125            }
3126            // LRM 9.6.2: `label` is a fork branch's own named outermost
3127            // block reachable from here -- the fork's parent (continuing
3128            // past it), a sibling branch, or a descendant of an ancestor
3129            // fork -- but not through a same-process lexically enclosing
3130            // block or this CFG's own `fork_name` (both already checked
3131            // above). Kept as an ordinary statement: codegen (`vj-
3132            // codegen::process_lower`) tells this case apart from the ones
3133            // above by `ProcessLoweringPlan::fork_branch_names` and lowers
3134            // it to a runtime scan/kill of every live, so-tagged process
3135            // reachable from the caller's own fork family
3136            // (`vj_sim::sched::SchedulerOps::disable_named`) -- never a
3137            // silent no-op when nothing is currently live (the same
3138            // "already finished, nothing to do" convention `disable fork;`
3139            // itself uses, LRM 9.6.3).
3140            if self.fork_branch_names.iter().any(|n| n == &label) {
3141                self.push(cur, stmt);
3142                return Ok(cur);
3143            }
3144            // LRM 9.6.2: `label` is a named block of the forking process
3145            // that encloses the fork this branch belongs to. The disable
3146            // ends all activity in the block: the siblings, this branch, and
3147            // the parent's wait at the join, which resumes after the block
3148            // (`sv_rt_disable_named`, the parent's `disable_scopes`). This
3149            // branch ends here, like `disable <fork name>;` above.
3150            if self.ancestor_blocks.iter().any(|n| n == &label) {
3151                self.push(cur, stmt);
3152                self.disable_broadcast.push((label, true));
3153                self.close(cur, Terminator::End { loop_back: None });
3154                return Ok(self.alloc_block());
3155            }
3156            return Err(LoweringError::unsupported(format!(
3157                "disable {label}; where `{label}` is not a named block or task enclosing it \
3158                 in the same process, a procedural assertion label of it, a uniquely named \
3159                 sequential block of another process or a task of the design (not a block \
3160                 inside a fork branch of another process, nor a name shared by two blocks), \
3161                 or a fork branch's own name reachable from here"
3162            )));
3163        }
3164        if kind == StatementKind::ForLoop {
3165            return self.walk_for_loop(stmt, cur);
3166        }
3167        if kind == StatementKind::WhileLoop {
3168            return self.walk_while_loop(stmt, cur);
3169        }
3170        if kind == StatementKind::DoWhileLoop {
3171            return self.walk_do_while_loop(stmt, cur);
3172        }
3173        if kind == StatementKind::ForeverLoop {
3174            return self.walk_forever_loop(stmt, cur);
3175        }
3176        if kind == StatementKind::ForeachLoop {
3177            return self.walk_foreach_loop(stmt, cur);
3178        }
3179        // R-run Task 8 (PR-045/PR-006): `repeat(N) body;` gets a real CFG
3180        // `Loop` shape ONLY when `body` contains a suspend point somewhere
3181        // in its own subtree -- a suspend inside a `repeat` needs its own
3182        // iteration counter to survive the resulting suspend/resume call
3183        // boundary (see `RepeatCounterId`'s own doc comment), which only a
3184        // real CFG split can provide. The suspend-free case stays exactly
3185        // the pre-existing opaque leaf `is_ordinary_statement_kind` already
3186        // routes to `stmt_shared::lower_repeat_loop` (R-run Task 4) --
3187        // falling through to that check below, unchanged, whenever this
3188        // condition is false, deliberately preserving
3189        // `repeat_loop_control_flow_shape_matches_between_function_and_
3190        // process_paths`'s own "a suspend-free repeat loop stays an
3191        // ordinary, un-decomposed leaf statement" invariant
3192        // (`shared_stmt_lowering.rs`).
3193        if kind == StatementKind::RepeatLoop
3194            && (contains_suspend_trigger(stmt.as_sem_node())
3195                || escapes(stmt.as_sem_node(), false, !self.return_targets.is_empty()))
3196        {
3197            return self.walk_repeat_loop(stmt, cur);
3198        }
3199
3200        // R-run Task 1: `$finish`/`$stop`/`$fatal` are a segment-closing
3201        // point, same category as a suspend -- LRM 1800-2023 §20.2/§20.10:
3202        // once one of these executes, none of the calling process's own
3203        // textually-following statements may ever run (not just the next
3204        // one in a flat statement list -- also not one nested arbitrarily
3205        // deep inside an `if`/`case`/loop branch this "ordinary straight-
3206        // line" arm below would otherwise happily keep pushing into the
3207        // SAME still-open block). This is checked before the generic
3208        // `is_ordinary_statement_kind` dispatch below (an `ExpressionStatement`
3209        // wrapping one of these calls is itself an ordinary-kind statement,
3210        // so without this earlier check it would fall straight into that
3211        // arm and keep accumulating every statement that follows it into
3212        // `cur`).
3213        //
3214        // Reuses the existing `Terminator::End { loop_back: None }` shape
3215        // (PR-069's "no new enum catch-all" extension policy) rather than a
3216        // new `Terminator::Halt`/`Terminator::Finish` variant: this closes
3217        // `cur` as a real process-kernel exit (`SegTerminator::End{loop_back:
3218        // None}` -> the compiled kernel returns `SEG_FINISHED` for this
3219        // call, per `lower_terminator`'s existing 1:1 mapping), unconditionally
3220        // `loop_back: None` even inside an `always`/`always_ff` process
3221        // (`ProcessLoopKind::Looping` would otherwise close the process's
3222        // FINAL block with `Some(entry)`) -- once `$finish`/`$stop`/`$fatal`
3223        // runs, the calling process itself must never be re-entered again,
3224        // regardless of what kind of process it is; the *global* simulation
3225        // shutdown that a bare `$finish`/`$stop` also requests is a
3226        // scheduler-level concern (`SchedulerOps::request_finish`,
3227        // `sv_rt_finish`'s own doc comment) this file has no visibility
3228        // into and does not need to model.
3229        //
3230        // Any statement textually following the call (whether later in this
3231        // same flat list, as `walk_seq`'s next loop iteration, or -- via
3232        // this function's own caller chain, e.g. `walk_conditional` closing
3233        // `then_exit`/`else_exit` with `Terminator::Fallthrough(join)` --
3234        // structurally nested deeper) is walked into a FRESH block instead
3235        // of `cur`: `cur` is already closed, so it can never be reached
3236        // again, and neither can anything later routed into the fresh
3237        // block, since nothing in this builder ever branches to a block
3238        // this function itself did not just hand back as "the current
3239        // exit" to some earlier, still-open predecessor -- there is none
3240        // here. The fresh block still gets a real terminator from whatever
3241        // caller eventually closes it (a plain, harmless `Fallthrough`/
3242        // `End` on dead code), satisfying `finish()`'s "every block must be
3243        // closed" invariant without special-casing unreachable blocks.
3244        // A call to a task whose body can suspend is inlined here, so its
3245        // timing controls become real suspend points of this process.
3246        if kind == StatementKind::ExpressionStatement
3247            && let Some(e) = stmt.expr()
3248            && crate::netlist::itf::is_vif_extern_call(&e)
3249        {
3250            return Err(LoweringError::unsupported(
3251                crate::netlist::itf::vif_extern_message(&e),
3252            ));
3253        }
3254        if kind == StatementKind::ExpressionStatement
3255            && let Some(e) = stmt.expr()
3256            && let Some(targets) = crate::netlist::itf::call_extern_targets(&e)
3257            && let Some(next) = self.walk_extern_call(stmt, e, &targets, cur)?
3258        {
3259            return Ok(next);
3260        }
3261        if kind == StatementKind::ExpressionStatement
3262            && let Some(e) = stmt.expr()
3263            && let Some(callee) = suspending_task_callee(&e)
3264        {
3265            return self.inline_task_call(e, callee, cur);
3266        }
3267        if matches!(kind, StatementKind::Break | StatementKind::Continue) {
3268            let &(cont, brk) = self
3269                .loop_targets
3270                .last()
3271                .ok_or_else(|| LoweringError::malformed(format!("{kind:?} outside any loop")))?;
3272            let target = if kind == StatementKind::Break {
3273                brk
3274            } else {
3275                cont
3276            };
3277            self.jump(cur, target);
3278            return Ok(self.alloc_block());
3279        }
3280        if (kind == StatementKind::Case || kind == StatementKind::PatternCase)
3281            && (contains_suspend_trigger(stmt.as_sem_node())
3282                || constant_case_live_arm(&stmt).is_some()
3283                || escapes(
3284                    stmt.as_sem_node(),
3285                    !self.loop_targets.is_empty(),
3286                    !self.return_targets.is_empty(),
3287                ))
3288        {
3289            return self.walk_case(stmt, cur);
3290        }
3291        if kind == StatementKind::ImmediateAssertion
3292            && let Some(actions) = suspending_assertion_actions(&stmt)
3293        {
3294            return self.walk_immediate_assertion(stmt, actions, cur);
3295        }
3296        if kind == StatementKind::ExpressionStatement
3297            && let Some(e) = stmt.expr()
3298            && e.kind() == ExpressionKind::Assignment
3299            && e.is_nonblocking() != Some(true)
3300            && e.assignment_timing().is_some()
3301        {
3302            return self.walk_intra_assignment_timing(e, cur);
3303        }
3304        // `q <= @(e) v;` / `q <= repeat (n) @(e) v;` (LRM 10.4.2). A plain
3305        // delay stays a statement: the staged write carries it.
3306        if kind == StatementKind::ExpressionStatement
3307            && let Some(e) = stmt.expr()
3308            && e.kind() == ExpressionKind::Assignment
3309            && e.is_nonblocking() == Some(true)
3310            && e.assignment_timing()
3311                .and_then(|n| n.as_timing_control())
3312                .is_some_and(|tc| tc.kind() != TimingControlKind::Delay)
3313        {
3314            return self.walk_nba_event(stmt, e, cur);
3315        }
3316        if kind == StatementKind::Return
3317            && let Some(&target) = self.return_targets.last()
3318        {
3319            // `return;` inside an inlined task: jump past the task body.
3320            // `return expr;` of an inlined function first assigns its result
3321            // variable (in a block of its own: bindings run on block entry).
3322            let mut cur = cur;
3323            if self.ret_var_stack.len() == self.return_targets.len()
3324                && let Some(Some(rv)) = self.ret_var_stack.last().copied()
3325                && let Some(value) = stmt.expr()
3326            {
3327                if let Some(next) = self.hoist_suspending_calls_in(value.as_sem_node(), cur)? {
3328                    cur = next;
3329                }
3330                let set = self.alloc_block();
3331                self.close(cur, Terminator::Fallthrough(set));
3332                self.blocks[set.0 as usize].bindings.push(ArgBinding::In {
3333                    formal: rv,
3334                    actual: value,
3335                });
3336                cur = set;
3337            }
3338            self.jump(cur, target);
3339            return Ok(self.alloc_block());
3340        }
3341        if kind == StatementKind::ExpressionStatement && is_finish_or_stop_call(&stmt) {
3342            self.push(cur, stmt);
3343            self.close(cur, Terminator::End { loop_back: None });
3344            return Ok(self.alloc_block());
3345        }
3346
3347        // PR-006's own acceptance text: "any other kind is ordinary
3348        // straight-line code". Kept as one opaque `Statement` leaf unless it
3349        // secretly contains a suspend-triggering construct somewhere in its
3350        // subtree, in which case there is no `Terminator::Loop`/`Case` to
3351        // represent the needed split (see this file's module doc comment's
3352        // extension policy) -- a named `LoweringError`, never a
3353        // silently-wrong CFG.
3354        if is_ordinary_statement_kind(kind) {
3355            if kind == StatementKind::ExpressionStatement
3356                && contains_suspend_trigger(stmt.as_sem_node())
3357                && let Some(next) = self.hoist_suspending_function_calls(stmt, cur)?
3358            {
3359                return Ok(next);
3360            }
3361            return if contains_suspend_trigger(stmt.as_sem_node())
3362                && !(is_expect(&stmt) && self.expect_as_leaf)
3363            {
3364                Err(LoweringError::unsupported(format!(
3365                    "suspend point nested inside a {kind:?} statement (no CFG terminator \
3366                     for this construct is implemented in this task, PR-069)"
3367                )))
3368            } else {
3369                self.push(cur, stmt);
3370                if kind == StatementKind::ExpressionStatement
3371                    && has_function_disables()
3372                    && contains_subroutine_call(stmt.as_sem_node())
3373                {
3374                    return Ok(self.fn_disable_check(cur));
3375                }
3376                Ok(cur)
3377            };
3378        }
3379
3380        // Unreachable as long as `determinism::kind_guard`'s pinned
3381        // `StatementKind::COUNT == 32` still holds -- every one of today's 32
3382        // variants is named above or in `is_ordinary_statement_kind`. A
3383        // `sv-lang-kinds` bump that adds a 33rd fails that pinned-count test
3384        // loudly; until this file is reviewed and updated, reaching here is a
3385        // named error, never a silent fallthrough.
3386        Err(LoweringError::malformed(format!(
3387            "unrecognized StatementKind {kind:?} (sv-lang-kinds version drift -- see \
3388             determinism::kind_guard's StatementKind pin)"
3389        )))
3390    }
3391
3392    /// `expect (property) pass else fail;` (LRM 1800-2023 §16.17): blocks the
3393    /// process until the assertion engine has decided the property.
3394    ///
3395    /// The statement appears twice, as the assertion's two halves. In the block
3396    /// it is reached in, it queues the evaluation (codegen's
3397    /// `sv_rt_proc_assert`); the process then parks with no sensitivity at all
3398    /// (an empty [`SuspendPoint::EdgeList`]) until the engine, in the Observed
3399    /// region of the step it decides in, wakes it. In the block it resumes into
3400    /// ([`BasicBlock::expect_resume`]) it runs the pass or `else` statement, and
3401    /// whatever follows the `expect` goes on from there.
3402    fn walk_expect(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
3403        let blocking = [stmt.assertion_if_true(), stmt.assertion_if_false()]
3404            .into_iter()
3405            .flatten()
3406            .any(|a| contains_suspend_trigger(a.as_sem_node()));
3407        if blocking {
3408            return self.walk_expect_blocking(stmt, cur);
3409        }
3410        self.push(cur, stmt);
3411        let resume = self.alloc_block();
3412        self.blocks[resume.0 as usize].expect_resume = true;
3413        self.close(
3414            cur,
3415            Terminator::Suspend {
3416                point: SuspendPoint::EdgeList(Vec::new(), Vec::new()),
3417                resume_blk: resume,
3418            },
3419        );
3420        self.push(resume, stmt);
3421        Ok(resume)
3422    }
3423
3424    /// An `expect` whose pass or `else` action blocks (a delay, an event
3425    /// control, a wait, another `expect`; LRM 1800-2023 16.17 allows any
3426    /// statement there). The action cannot run inline in the resume
3427    /// segment, so the resume block switches on the outcome the engine woke
3428    /// the process with (`sv_rt_fired_term`: 0 pass, 1 fail, 2 -- actions
3429    /// switched off by `$assertcontrol`, 20.11 -- neither) and walks each
3430    /// action as ordinary blocks. A fail with no `else` reports the `$error`
3431    /// through a zero-arm switch, as `wait_order` does.
3432    fn walk_expect_blocking(
3433        &mut self,
3434        stmt: Statement<'d>,
3435        cur: BlockId,
3436    ) -> Result<BlockId, LoweringError> {
3437        self.push(cur, stmt);
3438        let resume = self.alloc_block();
3439        self.close(
3440            cur,
3441            Terminator::Suspend {
3442                point: SuspendPoint::EdgeList(Vec::new(), Vec::new()),
3443                resume_blk: resume,
3444            },
3445        );
3446        let join = self.alloc_block();
3447        let pass = self.alloc_block();
3448        let fail = self.alloc_block();
3449        let pass_exit = match stmt.assertion_if_true() {
3450            Some(s) => self.walk_stmt(s, pass)?,
3451            None => pass,
3452        };
3453        self.close(pass_exit, Terminator::Fallthrough(join));
3454        let fail_exit = match stmt.assertion_if_false() {
3455            Some(s) => self.walk_stmt(s, fail)?,
3456            None => {
3457                self.close(
3458                    fail,
3459                    Terminator::Switch {
3460                        on: SwitchOn::FiredTerm,
3461                        arms: Vec::new(),
3462                        default_blk: join,
3463                        report: Some(stmt.source_range().start),
3464                    },
3465                );
3466                self.alloc_block()
3467            }
3468        };
3469        self.close(fail_exit, Terminator::Fallthrough(join));
3470        self.close(
3471            resume,
3472            Terminator::Switch {
3473                on: SwitchOn::FiredTerm,
3474                arms: vec![pass, fail, join],
3475                default_blk: join,
3476                report: None,
3477            },
3478        );
3479        Ok(join)
3480    }
3481
3482    /// A `randcase` whose items need their own blocks (one suspends, or
3483    /// escapes an enclosing loop or task): [`SwitchOn::RandCase`] picks one.
3484    fn walk_randcase(
3485        &mut self,
3486        stmt: Statement<'d>,
3487        cur: BlockId,
3488    ) -> Result<BlockId, LoweringError> {
3489        let join = self.alloc_block();
3490        let mut arms = Vec::new();
3491        for item in stmt.randcase_items() {
3492            let arm = self.alloc_block();
3493            arms.push(arm);
3494            let exit = self.walk_stmt(item.stmt(), arm)?;
3495            self.close(exit, Terminator::Fallthrough(join));
3496        }
3497        self.close(
3498            cur,
3499            Terminator::Switch {
3500                on: SwitchOn::RandCase(stmt),
3501                arms,
3502                default_blk: join,
3503                report: None,
3504            },
3505        );
3506        Ok(join)
3507    }
3508
3509    /// `wait_order(e1, ..., en) pass else fail;` (LRM 1800-2023 §15.5.4):
3510    /// step `i` waits on `e_i` .. `e_n`, and goes on only if `e_i` is the one
3511    /// that fired (the wait's term 0, [`SwitchOn::FiredTerm`]) -- any other
3512    /// fires out of order and fails. After `e_n`, the pass action; a
3513    /// failure without an `else` reports `$error`.
3514    fn walk_wait_order(
3515        &mut self,
3516        stmt: Statement<'d>,
3517        cur: BlockId,
3518    ) -> Result<BlockId, LoweringError> {
3519        let events = stmt.wait_order_events();
3520        if events.is_empty() {
3521            return Err(LoweringError::malformed("wait_order with no events"));
3522        }
3523        let pass = self.alloc_block();
3524        let fail = self.alloc_block();
3525        let join = self.alloc_block();
3526        let if_false = stmt.wait_order_if_false();
3527        let report = if_false.is_none().then(|| stmt.source_range().start);
3528        let mut at = cur;
3529        for i in 0..events.len() {
3530            let resume = self.alloc_block();
3531            self.close(
3532                at,
3533                Terminator::Suspend {
3534                    point: SuspendPoint::EdgeList(
3535                        events[i..]
3536                            .iter()
3537                            .map(|e| (*e, sv_lang::EdgeKind::None))
3538                            .collect(),
3539                        vec![Vec::new(); events.len() - i],
3540                    ),
3541                    resume_blk: resume,
3542                },
3543            );
3544            let next = if i + 1 == events.len() {
3545                pass
3546            } else {
3547                self.alloc_block()
3548            };
3549            self.close(
3550                resume,
3551                Terminator::Switch {
3552                    on: SwitchOn::FiredTerm,
3553                    arms: vec![next],
3554                    default_blk: fail,
3555                    report,
3556                },
3557            );
3558            at = next;
3559        }
3560        let pass_exit = match stmt.wait_order_if_true() {
3561            Some(s) => self.walk_stmt(s, pass)?,
3562            None => pass,
3563        };
3564        self.close(pass_exit, Terminator::Fallthrough(join));
3565        let fail_exit = match if_false {
3566            Some(s) => self.walk_stmt(s, fail)?,
3567            None => fail,
3568        };
3569        self.close(fail_exit, Terminator::Fallthrough(join));
3570        Ok(join)
3571    }
3572
3573    /// Inlines `call` (to `task`, whose body can suspend) at `cur`: a fresh
3574    /// entry block binds the formals ([`ArgBinding::In`]/[`ArgBinding::
3575    /// Init`]) and initializes the task's locals, the body is walked in
3576    /// place (a `return;` jumps to the continuation), and the continuation
3577    /// block starts with the output copy-backs ([`ArgBinding::Out`]).
3578    fn inline_task_call(
3579        &mut self,
3580        call: Expression<'d>,
3581        task: Symbol<'d>,
3582        cur: BlockId,
3583    ) -> Result<BlockId, LoweringError> {
3584        let result_temp = self.pending_result_temp.take();
3585        // Through a virtual interface (`vif.t(...)`): the task of the
3586        // instance it selects -- one inlined copy per candidate instance
3587        // (`netlist::vif`), picked by its id; `null` runs nothing.
3588        if let Some((vif, targets)) = crate::netlist::vif::call_targets(&call) {
3589            let actuals: Vec<Expression<'d>> = call.operands()[1..].to_vec();
3590            let join = self.alloc_block();
3591            let mut arms = Vec::new();
3592            for target in targets {
3593                let arm = self.alloc_block();
3594                arms.push(arm);
3595                match target {
3596                    Some(t) => {
3597                        let exit = self.inline_task_body(&actuals, t, arm, None)?;
3598                        self.park_call_result(exit, result_temp, t);
3599                        self.close(exit, Terminator::Fallthrough(join));
3600                    }
3601                    None => self.close(arm, Terminator::Fallthrough(join)),
3602                }
3603            }
3604            self.close(
3605                cur,
3606                Terminator::Switch {
3607                    on: SwitchOn::VifId(vif),
3608                    arms,
3609                    default_blk: join,
3610                    report: None,
3611                },
3612            );
3613            return Ok(join);
3614        }
3615        // A class-method task (LRM 8.6): its `this` is bound to the receiver
3616        // (`obj.run()`), or for an implicit-`this` call (`run()` inside
3617        // another method) to the caller's current object, and kept in a frame
3618        // local across the suspend points.
3619        self.call_anchor = Some(call);
3620        let receiver = call.call_this_class();
3621        let mut actuals = call.operands();
3622        if receiver.is_some() && !actuals.is_empty() {
3623            actuals.remove(0);
3624        }
3625        // A `pure virtual` task has no body and so no `this` of its own; its
3626        // calls still dispatch over the implementations below (LRM 8.21).
3627        let pure = task.subroutine_flags().contains(sv_lang::MethodFlags::PURE);
3628        let this_var = task.subroutine_this_var();
3629        if this_var.is_none() && !pure {
3630            let exit = self.inline_task_body(&actuals, task, cur, None)?;
3631            self.park_call_result(exit, result_temp, task);
3632            return Ok(exit);
3633        }
3634        // A virtual task is bound by the object's class at run time (LRM
3635        // 8.20): one inlined override per implementation the possible dynamic
3636        // classes resolve it to, picked by the object's class. A call through
3637        // `super.` is not virtual.
3638        if !(receiver.is_none() && is_explicit_super(call))
3639            && (task
3640                .subroutine_flags()
3641                .contains(sv_lang::MethodFlags::VIRTUAL)
3642                || task.subroutine_override().is_some())
3643        {
3644            let impls = self
3645                .virtual_tasks
3646                .as_ref()
3647                .map(|v| v.implementations(&task))
3648                .unwrap_or_default();
3649            // No class the design creates implements it: no object can
3650            // reach the call, so it can only run on a null handle (an
3651            // error at run time, as for any call through `null`). E.g. UVM's
3652            // `uvm_sequencer_base::start_phase_sequence` in a bench with no
3653            // sequencer.
3654            let join = self.alloc_block();
3655            let mut arms = Vec::new();
3656            let mut ids = Vec::new();
3657            for (class_ids, imp) in impls {
3658                let this_var = imp.subroutine_this_var().ok_or_else(|| {
3659                    LoweringError::malformed(format!(
3660                        "override of virtual task `{}` has no `this`",
3661                        task.name()
3662                    ))
3663                })?;
3664                let arm = self.alloc_block();
3665                arms.push(arm);
3666                ids.push(class_ids);
3667                let exit = self.inline_task_body(&actuals, imp, arm, Some((this_var, receiver)))?;
3668                self.park_call_result(exit, result_temp, imp);
3669                self.close(exit, Terminator::Fallthrough(join));
3670            }
3671            self.close(
3672                cur,
3673                Terminator::Switch {
3674                    on: SwitchOn::ObjClass { receiver, ids },
3675                    arms,
3676                    default_blk: join,
3677                    // A null (or dead) handle runs no override: an error.
3678                    report: Some(call.source_range().start),
3679                },
3680            );
3681            return Ok(join);
3682        }
3683        let this_var = this_var.ok_or_else(|| {
3684            LoweringError::malformed(format!("method task `{}` has no `this`", task.name()))
3685        })?;
3686        let exit = self.inline_task_body(&actuals, task, cur, Some((this_var, receiver)))?;
3687        self.park_call_result(exit, result_temp, task);
3688        Ok(exit)
3689    }
3690
3691    /// At the end of an inlined function `imp` (block `exit`): copies its
3692    /// result variable into the call's capture temporary, when it has one.
3693    fn park_call_result(
3694        &mut self,
3695        exit: BlockId,
3696        result_temp: Option<(u32, Expression<'d>)>,
3697        imp: Symbol<'d>,
3698    ) {
3699        if let Some((temp, _)) = result_temp
3700            && let Some(rv) = imp.subroutine_return_val_var()
3701        {
3702            self.blocks[exit.0 as usize]
3703                .bindings
3704                .push(ArgBinding::ToCaptured { temp, formal: rv });
3705        }
3706    }
3707
3708    /// The first call of a task that calls itself (directly or through other
3709    /// tasks) and contains timing controls (LRM 1800-2023 §13.4, §13.4.1):
3710    /// inlining cannot bound the depth, so each activation is a forked child
3711    /// of its caller that the caller joins. The caller evaluates the
3712    /// arguments now ([`ArgBinding::Capture`]), the child copies them into
3713    /// its own formals ([`ArgBinding::FromCaptured`]) and runs the body in a
3714    /// frame of its own (automatic variables, §6.21); a call inside the body
3715    /// forks the same kernel again ([`Self::recursive_self_call`]).
3716    ///
3717    /// `output` and `inout` formals (LRM 1800-2023 13.5) are copied out when
3718    /// the activation ends: parked in a temporary ([`ArgBinding::ToCaptured`]),
3719    /// handed to the spawner's own temporary ([`InheritedSlot::Return`]) and
3720    /// assigned to the actual after the join ([`ArgBinding::AssignCaptured`]).
3721    /// They are limited to what a capture temporary holds (a packed value of
3722    /// at most 128 bits).
3723    ///
3724    /// A class method (LRM 1800-2023 8.11) also takes its `this` as capture 0
3725    /// (an implicit receiver by [`ArgBinding::CaptureThis`]). A recursive
3726    /// void function that spawns processes is handled the same way; its
3727    /// `join_none` children may then start earlier than in a call that never
3728    /// yields (the caller waits at each recursive join).
3729    ///
3730    /// Deviation: the activation is a fork child, so `disable fork` and
3731    /// `wait fork` of the caller see it; only tasks with automatic lifetime
3732    /// are supported (`ref` / static formals and a `disable` of the task fail
3733    /// by name).
3734    fn inline_recursive_task(
3735        &mut self,
3736        actuals: &[Expression<'d>],
3737        task: Symbol<'d>,
3738        cur: BlockId,
3739        this: Option<(Symbol<'d>, Option<Expression<'d>>)>,
3740    ) -> Result<BlockId, LoweringError> {
3741        let refuse = |why: &str| {
3742            LoweringError::unsupported(format!(
3743                "recursive call to task `{}`, which contains timing controls: {why} \
3744                 (LRM 1800-2023 13.4.1)",
3745                task.name()
3746            ))
3747        };
3748        let body = proc_body(&task).ok_or_else(|| {
3749            LoweringError::malformed(format!("task `{}` has no body", task.name()))
3750        })?;
3751        let mut formals: Vec<Symbol<'d>> = task.subroutine_arguments().collect();
3752        if formals.len() != actuals.len() {
3753            return Err(LoweringError::malformed(format!(
3754                "call to task `{}` has {} arguments for {} formals",
3755                task.name(),
3756                actuals.len(),
3757                formals.len()
3758            )));
3759        }
3760        if task.subroutine_kind() == Some(sv_lang::SubroutineKind::Function)
3761            && task.subroutine_return_type().is_some_and(|t| !t.is_void())
3762        {
3763            return Err(refuse("a value-returning function is not supported"));
3764        }
3765        for f in &formals {
3766            if crate::netlist::is_static_procedural_var(f) {
3767                return Err(refuse("a recursive task must be `automatic`"));
3768            }
3769        }
3770        // slang binds an output/inout actual as `actual = <formal>`: the
3771        // actual is its target.
3772        let mut actuals: Vec<Expression<'d>> = actuals
3773            .iter()
3774            .zip(&formals)
3775            .map(|(&a, f)| {
3776                if f.formal_argument_direction() == sv_lang::ArgumentDirection::In {
3777                    a
3778                } else {
3779                    call_actual_lvalue(a)
3780                }
3781            })
3782            .collect();
3783        // A class method: its `this` is formal 0, an input handle.
3784        let has_this = this.is_some();
3785        let mut implicit_this = false;
3786        if let Some((this_var, recv)) = this {
3787            let anchor = self.call_anchor.ok_or_else(|| {
3788                LoweringError::malformed("recursive method call without a call expression")
3789            })?;
3790            implicit_this = recv.is_none();
3791            formals.insert(0, this_var);
3792            actuals.insert(0, recv.unwrap_or(anchor));
3793        }
3794        let n = formals.len() as u32;
3795        let actuals = &actuals[..];
3796        // A `ref` formal bound to static storage (a variable or a net, directly or
3797        // through an enclosing call's aliased `ref`) is that storage in every
3798        // activation (LRM 1800-2023 13.5.2): no capture, no copy-back. Every
3799        // recursive call must bind it to the same storage
3800        // ([`Self::recursive_self_call`]); anything else is refused by name.
3801        let mut ref_alias: Vec<Option<Symbol<'d>>> = vec![None; formals.len()];
3802        for (i, f) in formals.iter().enumerate() {
3803            if f.formal_argument_direction() != sv_lang::ArgumentDirection::Ref {
3804                continue;
3805            }
3806            let is_event = f
3807                .value_type()
3808                .as_ref()
3809                .is_some_and(|t| crate::state::event_pool::event_count(t).is_some());
3810            match self.ref_storage(actuals[i]) {
3811                Some(t) if !is_event => ref_alias[i] = Some(t),
3812                _ => {
3813                    return Err(refuse(
3814                        "a `ref` argument must name a static variable or net (or an \
3815                         enclosing task's aliased `ref` formal)",
3816                    ));
3817                }
3818            }
3819        }
3820        let is_in = |i: usize| {
3821            (has_this && i == 0)
3822                || formals[i].formal_argument_direction() == sv_lang::ArgumentDirection::In
3823                || ref_alias[i].is_some()
3824        };
3825        let outs: Vec<usize> = (0..formals.len()).filter(|&i| !is_in(i)).collect();
3826        let groups = if outs.is_empty() { 2 } else { 4 };
3827
3828        // The branch: its own builder. Capture temporaries `0..n` hold the
3829        // incoming arguments, `n..2n` those of a recursive call; with
3830        // `output`/`inout` formals `2n..3n` hold this activation's results
3831        // and `3n..4n` those of a recursive call.
3832        // The same task activated from the same context builds the same
3833        // Root: build it once per process ([`RootKey`]) and the lowering
3834        // compiles it once ([`ProcessCfg::share_id`]); only the call site's
3835        // bindings below are per site. Not under a clock inference.
3836        let key = (self.global_clock.is_none() && self.checker_clock.is_none()).then(|| {
3837            MemoKey::Root(RootKey {
3838                task: task.uid(),
3839                this_var: this.map(|(t, _)| t.uid()),
3840                default_clocking: self.default_clocking.map(|c| c.uid()),
3841                rec_outer: self.rec_outer.clone(),
3842                chain: self
3843                    .rec_up
3844                    .iter()
3845                    .chain(self.rec.iter())
3846                    .map(|r| r.share)
3847                    .collect(),
3848                ref_aliases: self
3849                    .ref_stack
3850                    .iter()
3851                    .map(|(f, t)| (f.uid(), t.uid()))
3852                    .chain(
3853                        formals
3854                            .iter()
3855                            .zip(&ref_alias)
3856                            .filter_map(|(f, t)| t.map(|t| (f.uid(), t.uid()))),
3857                    )
3858                    .collect(),
3859                labels: self.fork_branch_names.clone(),
3860                ancestors: self.ancestor_blocks.clone(),
3861            })
3862        });
3863        let cached = key
3864            .as_ref()
3865            .and_then(|k| self.fork_memo.borrow().get(k).cloned());
3866        let mut branch = if let Some(hit) = cached {
3867            hit
3868        } else {
3869            let share = NEXT_SHARE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3870            let mut b = CfgBuilder::new();
3871            b.fork_memo = self.fork_memo.clone();
3872            b.global_clock = self.global_clock;
3873            b.virtual_tasks = self.virtual_tasks.clone();
3874            b.fork_branch_names = self.fork_branch_names.clone();
3875            b.ancestor_blocks = self.ancestor_blocks.clone();
3876            b.default_clocking = self.default_clocking;
3877            b.checker_clock = self.checker_clock;
3878            b.rec_outer = self.rec_outer.clone();
3879            b.rec_outer.push(task.uid());
3880            b.rec_up = self.rec_up.clone();
3881            b.rec_up.extend(self.rec.clone());
3882            b.rec = Some(RecursiveTask {
3883                task: task.uid(),
3884                share,
3885                proto: actuals
3886                    .iter()
3887                    .copied()
3888                    .zip(formals.iter().copied())
3889                    .collect(),
3890                n,
3891                groups,
3892                outs: (0..formals.len()).map(|i| !is_in(i)).collect(),
3893                refs: ref_alias.clone(),
3894                has_this,
3895                body,
3896            });
3897            b.ref_stack = self.ref_stack.clone();
3898            for (f, t) in formals.iter().zip(&ref_alias) {
3899                if let Some(t) = t {
3900                    b.ref_stack.push((*f, *t));
3901                    b.ref_candidates.push((*f, Some(*t)));
3902                }
3903            }
3904            if let Some((this_var, _)) = this {
3905                b.this_stack.push(this_var);
3906            }
3907            for _ in 0..groups {
3908                for (f, &a) in formals.iter().zip(actuals) {
3909                    b.set_capture_type(a, *f);
3910                }
3911            }
3912            let entry = b.alloc_block();
3913            for (i, f) in formals.iter().enumerate() {
3914                // An `output` formal starts out as its type's default.
3915                if ref_alias[i].is_some() {
3916                    continue;
3917                }
3918                b.blocks[entry.0 as usize].bindings.push(
3919                    if !is_in(i) && f.formal_argument_direction() == sv_lang::ArgumentDirection::Out
3920                    {
3921                        ArgBinding::Init { var: *f }
3922                    } else {
3923                        ArgBinding::FromCaptured {
3924                            temp: i as u32,
3925                            formal: *f,
3926                        }
3927                    },
3928                );
3929                b.inlined_locals.push(*f);
3930            }
3931            for local in task_locals(&task) {
3932                // A method's `this` is bound above (formal 0).
3933                if this.is_some_and(|(t, _)| t.uid() == local.uid()) {
3934                    continue;
3935                }
3936                if crate::netlist::is_static_procedural_var(&local) {
3937                    continue;
3938                }
3939                if !is_automatic_block_var(&local) {
3940                    b.blocks[entry.0 as usize]
3941                        .bindings
3942                        .push(ArgBinding::Init { var: local });
3943                }
3944                b.inlined_locals.push(local);
3945            }
3946            for v in crate::patterns::pattern_vars(body.as_sem_node()) {
3947                if !b.inlined_locals.iter().any(|l| l.uid() == v.uid()) {
3948                    b.inlined_locals.push(v);
3949                }
3950            }
3951            let after = b.alloc_block();
3952            b.target_depth.insert(after, 0);
3953            b.return_targets.push(after);
3954            // `disable task;` ends every activation (LRM 9.6.2): each one's exit
3955            // is the end of its own kernel.
3956            let task_name = task.name().to_string();
3957            let task_scoped = is_disabled_somewhere(&task_name);
3958            if task_scoped {
3959                b.scope_chain
3960                    .push((task_name.clone(), after, b.frame_stack.len()));
3961                b.disable_targets
3962                    .push((task_name, after, b.frame_stack.len()));
3963            }
3964            // LRM 19.3 / 19.5.5: every activation signals the task's `begin` at
3965            // its first statement and `end` at its exit block (also reached by a
3966            // `disable task;`), like an inlined call.
3967            b.block_event(entry, task, false);
3968            b.event_stack.push((task, b.disable_targets.len()));
3969            let exit = b.walk_stmt(body, entry);
3970            b.event_stack.pop();
3971            if task_scoped {
3972                b.scope_chain.pop();
3973                b.disable_targets.pop();
3974            }
3975            let exit = exit?;
3976            b.close(exit, Terminator::Fallthrough(after));
3977            b.block_event(after, task, true);
3978            for &i in &outs {
3979                b.blocks[after.0 as usize]
3980                    .bindings
3981                    .push(ArgBinding::ToCaptured {
3982                        temp: 2 * n + i as u32,
3983                        formal: formals[i],
3984                    });
3985            }
3986            b.close(after, Terminator::End { loop_back: None });
3987            let inlined_locals = std::mem::take(&mut b.inlined_locals);
3988            let captures = std::mem::take(&mut b.captures);
3989            let capture_types = std::mem::take(&mut b.capture_types);
3990            let b_ref_formals: Vec<Symbol<'d>> = b.ref_stack.iter().map(|&(f, _)| f).collect();
3991            let ref_bindings = b.take_ref_bindings();
3992            let disable_scopes = b.take_disable_scopes();
3993            let disable_broadcast = std::mem::take(&mut b.disable_broadcast);
3994            let fork_branch_names = std::mem::take(&mut b.fork_branch_names);
3995            let mut built = ProcessCfg {
3996                blocks: b.finish(),
3997                entry,
3998                body,
3999                inlined_locals,
4000                captures,
4001                captured: Vec::new(),
4002                ref_bindings,
4003                disable_names: Vec::new(),
4004                fork_branch_names,
4005                disable_scopes,
4006                disable_broadcast,
4007                inherited: Vec::new(),
4008                capture_types,
4009                recursion: Recursion::Root,
4010                share_id: None,
4011            };
4012            built.captured = captured_vars(&built);
4013            built
4014                .captured
4015                .retain(|c| !b_ref_formals.iter().any(|f| f.uid() == c.uid()));
4016            if let Some(k) = key {
4017                built.share_id = Some(share);
4018                self.fork_memo.borrow_mut().insert(k, built.clone());
4019            }
4020            built
4021        };
4022
4023        // The caller: the arguments, evaluated now, into temporaries the
4024        // child inherits by value.
4025        let call = self.alloc_block();
4026        self.close(cur, Terminator::Fallthrough(call));
4027        for (i, (f, &a)) in formals.iter().zip(actuals).enumerate() {
4028            let temp = self.set_capture_type(a, *f);
4029            self.blocks[call.0 as usize]
4030                .bindings
4031                .push(if i == 0 && implicit_this {
4032                    ArgBinding::CaptureThis { temp }
4033                } else {
4034                    ArgBinding::Capture { temp, value: a }
4035                });
4036            branch.inherited.push(InheritedSlot::Temp {
4037                parent: temp,
4038                child: i as u32,
4039            });
4040        }
4041        let join_blk = self.alloc_block();
4042        // The results, back into the actuals once the activation is done.
4043        for &i in &outs {
4044            let ret = self.set_capture_type(actuals[i], formals[i]);
4045            branch.inherited.push(InheritedSlot::Return {
4046                parent: ret,
4047                child: 2 * n + i as u32,
4048            });
4049            self.blocks[join_blk.0 as usize]
4050                .bindings
4051                .push(ArgBinding::AssignCaptured {
4052                    temp: ret,
4053                    target: actuals[i],
4054                    nba: false,
4055                });
4056        }
4057        self.close(
4058            call,
4059            Terminator::ForkJoin {
4060                kind: StatementBlockKind::JoinAll,
4061                branches: vec![branch],
4062                join_blk,
4063            },
4064        );
4065        Ok(join_blk)
4066    }
4067
4068    /// A call of the recursive task inside its own activation
4069    /// ([`Self::inline_recursive_task`]): the arguments go to capture
4070    /// temporaries `n..2n`, and a placeholder branch forks the activation's
4071    /// own kernel again ([`Recursion::SelfSpawn`]), joined at once.
4072    fn recursive_self_call(
4073        &mut self,
4074        actuals: &[Expression<'d>],
4075        cur: BlockId,
4076        recv: Option<Option<Expression<'d>>>,
4077    ) -> Result<BlockId, LoweringError> {
4078        let rec = self
4079            .rec
4080            .as_ref()
4081            .ok_or_else(|| LoweringError::malformed("recursive call outside its activation"))?;
4082        let (n, body, rec_outs) = (rec.n, rec.body, rec.outs.clone());
4083        let skip = usize::from(rec.has_this);
4084        for (a, t) in actuals.iter().zip(&rec.refs[skip..]) {
4085            if let Some(t) = t
4086                && self.ref_storage(call_actual_lvalue(*a)).map(|s| s.uid()) != Some(t.uid())
4087            {
4088                return Err(LoweringError::unsupported(
4089                    "recursive call whose `ref` argument is not the storage the first call \
4090                     bound (a recursive task's `ref` formal is an alias of one static \
4091                     variable or net; LRM 1800-2023 13.5.2)",
4092                ));
4093            }
4094        }
4095        let mut actuals: Vec<Expression<'d>> = actuals
4096            .iter()
4097            .zip(&rec_outs[skip..])
4098            .map(|(&a, &out)| if out { call_actual_lvalue(a) } else { a })
4099            .collect();
4100        // The receiver of a method call is capture 0.
4101        let mut implicit_this = false;
4102        if let Some(r) = recv {
4103            let anchor = self.call_anchor.ok_or_else(|| {
4104                LoweringError::malformed("recursive method call without a call expression")
4105            })?;
4106            implicit_this = r.is_none();
4107            actuals.insert(0, r.unwrap_or(anchor));
4108        }
4109        let actuals = &actuals[..];
4110        if actuals.len() as u32 != n {
4111            return Err(LoweringError::malformed(
4112                "recursive call with the wrong number of arguments",
4113            ));
4114        }
4115        let call = self.alloc_block();
4116        self.close(cur, Terminator::Fallthrough(call));
4117        let mut inherited = Vec::new();
4118        for (i, &a) in actuals.iter().enumerate() {
4119            let temp = n + i as u32;
4120            self.blocks[call.0 as usize]
4121                .bindings
4122                .push(if i == 0 && implicit_this {
4123                    ArgBinding::CaptureThis { temp }
4124                } else {
4125                    ArgBinding::Capture { temp, value: a }
4126                });
4127            inherited.push(InheritedSlot::Temp {
4128                parent: temp,
4129                child: i as u32,
4130            });
4131        }
4132        // The results of the callee, back into the actuals after the join.
4133        let join_blk = self.alloc_block();
4134        for (i, &a) in actuals.iter().enumerate() {
4135            if rec_outs[i] {
4136                let ret = 3 * n + i as u32;
4137                inherited.push(InheritedSlot::Return {
4138                    parent: ret,
4139                    child: 2 * n + i as u32,
4140                });
4141                self.blocks[join_blk.0 as usize]
4142                    .bindings
4143                    .push(ArgBinding::AssignCaptured {
4144                        temp: ret,
4145                        target: a,
4146                        nba: false,
4147                    });
4148            }
4149        }
4150        let mut stub = CfgBuilder::new();
4151        let entry = stub.alloc_block();
4152        stub.close(entry, Terminator::End { loop_back: None });
4153        let branch = ProcessCfg {
4154            blocks: stub.finish(),
4155            entry,
4156            body,
4157            inlined_locals: Vec::new(),
4158            captures: Vec::new(),
4159            captured: Vec::new(),
4160            ref_bindings: Vec::new(),
4161            disable_names: Vec::new(),
4162            fork_branch_names: Vec::new(),
4163            disable_scopes: Vec::new(),
4164            disable_broadcast: Vec::new(),
4165            inherited,
4166            capture_types: Vec::new(),
4167            recursion: Recursion::SelfSpawn,
4168            share_id: None,
4169        };
4170        self.close(
4171            call,
4172            Terminator::ForkJoin {
4173                kind: StatementBlockKind::JoinAll,
4174                branches: vec![branch],
4175                join_blk,
4176            },
4177        );
4178        Ok(join_blk)
4179    }
4180
4181    /// A call, inside one activation, of the task of the enclosing activation
4182    /// `self.rec_up[k]` (mutual recursion, LRM 1800-2023 13.4.1): the
4183    /// arguments go to fresh capture temporaries and a placeholder branch
4184    /// forks that activation's kernel again ([`Recursion::AncestorSpawn`]),
4185    /// joined at once.
4186    fn recursive_ancestor_call(
4187        &mut self,
4188        k: usize,
4189        actuals: &[Expression<'d>],
4190        cur: BlockId,
4191        recv: Option<Option<Expression<'d>>>,
4192    ) -> Result<BlockId, LoweringError> {
4193        let anc = self.rec_up[k].clone();
4194        if anc.refs.iter().any(Option::is_some) {
4195            return Err(LoweringError::unsupported(
4196                "mutually recursive tasks with a `ref` formal (LRM 1800-2023 13.4.1, 13.5.2)",
4197            ));
4198        }
4199        let skip = usize::from(anc.has_this);
4200        let mut actuals: Vec<Expression<'d>> = actuals
4201            .iter()
4202            .zip(&anc.outs[skip..])
4203            .map(|(&a, &out)| if out { call_actual_lvalue(a) } else { a })
4204            .collect();
4205        let mut implicit_this = false;
4206        if let Some(r) = recv {
4207            let anchor = self.call_anchor.ok_or_else(|| {
4208                LoweringError::malformed("recursive method call without a call expression")
4209            })?;
4210            implicit_this = r.is_none();
4211            actuals.insert(0, r.unwrap_or(anchor));
4212        }
4213        if actuals.len() as u32 != anc.n {
4214            return Err(LoweringError::malformed(
4215                "recursive call with the wrong number of arguments",
4216            ));
4217        }
4218        let call = self.alloc_block();
4219        self.close(cur, Terminator::Fallthrough(call));
4220        let mut inherited = Vec::new();
4221        for (i, &a) in actuals.iter().enumerate() {
4222            let temp = self.set_capture_type(a, anc.proto[i].1);
4223            self.blocks[call.0 as usize]
4224                .bindings
4225                .push(if i == 0 && implicit_this {
4226                    ArgBinding::CaptureThis { temp }
4227                } else {
4228                    ArgBinding::Capture { temp, value: a }
4229                });
4230            inherited.push(InheritedSlot::Temp {
4231                parent: temp,
4232                child: i as u32,
4233            });
4234        }
4235        // The ancestor's results, back into the actuals after the join.
4236        let join_blk = self.alloc_block();
4237        for (i, &a) in actuals.iter().enumerate() {
4238            if anc.outs[i] {
4239                let ret = self.set_capture_type(a, anc.proto[i].1);
4240                inherited.push(InheritedSlot::Return {
4241                    parent: ret,
4242                    child: 2 * anc.n + i as u32,
4243                });
4244                self.blocks[join_blk.0 as usize]
4245                    .bindings
4246                    .push(ArgBinding::AssignCaptured {
4247                        temp: ret,
4248                        target: a,
4249                        nba: false,
4250                    });
4251            }
4252        }
4253        let mut stub = CfgBuilder::new();
4254        let entry = stub.alloc_block();
4255        stub.close(entry, Terminator::End { loop_back: None });
4256        let branch = ProcessCfg {
4257            blocks: stub.finish(),
4258            entry,
4259            body: anc.body,
4260            inlined_locals: Vec::new(),
4261            captures: Vec::new(),
4262            captured: Vec::new(),
4263            ref_bindings: Vec::new(),
4264            disable_names: Vec::new(),
4265            fork_branch_names: Vec::new(),
4266            disable_scopes: Vec::new(),
4267            disable_broadcast: Vec::new(),
4268            inherited,
4269            capture_types: Vec::new(),
4270            recursion: Recursion::AncestorSpawn((self.rec_up.len() - k) as u32),
4271            share_id: None,
4272        };
4273        self.close(
4274            call,
4275            Terminator::ForkJoin {
4276                kind: StatementBlockKind::JoinAll,
4277                branches: vec![branch],
4278                join_blk,
4279            },
4280        );
4281        Ok(join_blk)
4282    }
4283
4284    /// A new capture temporary shaped like `formal`, holding `value`.
4285    fn set_capture_type(&mut self, value: Expression<'d>, formal: Symbol<'d>) -> u32 {
4286        let temp = self.captures.len() as u32;
4287        self.captures.push(value);
4288        self.capture_types.resize(temp as usize, None);
4289        self.capture_types.push(Some(formal));
4290        temp
4291    }
4292
4293    /// [`Self::inline_task_call`] of `task` with the arguments `actuals`.
4294    /// An expression statement that calls a value-returning function whose
4295    /// body spawns processes (`fork ... join_none` in a function, LRM
4296    /// 1800-2023 13.4.4), which cannot be compiled: each such call is inlined
4297    /// ahead of the statement (arguments first, in source order) and the
4298    /// statement reads the function's result variable instead of calling it
4299    /// (codegen `lower_user_subroutine_call_of`). `None` when the statement
4300    /// has no such call. The same function twice in one statement, or a call
4301    /// to a virtual method, is refused by name.
4302    fn hoist_suspending_function_calls(
4303        &mut self,
4304        stmt: Statement<'d>,
4305        cur: BlockId,
4306    ) -> Result<Option<BlockId>, LoweringError> {
4307        let Some(cur) = self.hoist_suspending_calls_in(stmt.as_sem_node(), cur)? else {
4308            return Ok(None);
4309        };
4310        self.push(cur, stmt);
4311        Ok(Some(cur))
4312    }
4313
4314    /// [`Self::hoist_suspending_function_calls`]' inlining half over any
4315    /// expression tree: inlines every spawning function call under `node`
4316    /// ahead of the caller, which then reads their result variables. Also
4317    /// used for an inlined function's `return expr;` (`return m(...)`).
4318    fn hoist_suspending_calls_in(
4319        &mut self,
4320        node: SemNode<'d>,
4321        cur: BlockId,
4322    ) -> Result<Option<BlockId>, LoweringError> {
4323        let mut calls = Vec::new();
4324        collect_suspending_function_calls(node, &mut calls);
4325        if calls.is_empty() {
4326            return Ok(None);
4327        }
4328        let mut count: std::collections::HashMap<_, usize> = std::collections::HashMap::new();
4329        for call in &calls {
4330            if let Some(callee) = user_task_callee(call) {
4331                *count.entry(callee.uid()).or_default() += 1;
4332            }
4333        }
4334        let mut cur = cur;
4335        for call in calls {
4336            let Some(callee) = user_task_callee(&call) else {
4337                continue;
4338            };
4339            // A method (LRM 8.6) is inlined with its `this` bound to the
4340            // receiver (`inline_task_call`). A virtual one picks among
4341            // several overrides, each with a result variable of its own.
4342            let is_method =
4343                callee.subroutine_this_var().is_some() || call.call_this_class().is_some();
4344            let flags = callee.subroutine_flags();
4345            let is_virtual = is_method
4346                && (flags.contains(sv_lang::MethodFlags::VIRTUAL)
4347                    || flags.contains(sv_lang::MethodFlags::PURE)
4348                    || callee.subroutine_override().is_some());
4349            // The function's result variable is shared by its calls (and a
4350            // virtual method's overrides have one each): a call that cannot
4351            // read it straight after its inlining parks the value in a
4352            // temporary of its own, which the call's expression reads.
4353            let temp = (is_virtual || count.get(&callee.uid()).copied().unwrap_or(0) > 1)
4354                .then(|| {
4355                    callee
4356                        .subroutine_return_val_var()
4357                        .map(|rv| self.set_capture_type(call, rv))
4358                })
4359                .flatten();
4360            if let Some(temp) = temp {
4361                self.pending_result_temp = Some((temp, call));
4362            }
4363            cur = if is_method {
4364                self.inline_task_call(call, callee, cur)?
4365            } else {
4366                let actuals = call.operands();
4367                let exit = self.inline_task_body(&actuals, callee, cur, None)?;
4368                self.park_call_result(exit, temp.map(|t| (t, call)), callee);
4369                exit
4370            };
4371            self.pending_result_temp = None;
4372            if let Some(temp) = temp {
4373                self.blocks[cur.0 as usize]
4374                    .bindings
4375                    .push(ArgBinding::CallResult { temp, call });
4376            }
4377        }
4378        Ok(Some(cur))
4379    }
4380
4381    fn inline_task_body(
4382        &mut self,
4383        actuals: &[Expression<'d>],
4384        task: Symbol<'d>,
4385        cur: BlockId,
4386        this: Option<(Symbol<'d>, Option<Expression<'d>>)>,
4387    ) -> Result<BlockId, LoweringError> {
4388        if let Some(rec) = &self.rec
4389            && rec.task == task.uid()
4390            && this.is_some() == rec.has_this
4391        {
4392            return self.recursive_self_call(actuals, cur, this.map(|t| t.1));
4393        }
4394        if let Some(k) = self
4395            .rec_up
4396            .iter()
4397            .rposition(|r| r.task == task.uid() && r.has_this == this.is_some())
4398        {
4399            return self.recursive_ancestor_call(k, actuals, cur, this.map(|t| t.1));
4400        }
4401        if self.rec.is_none() && self.rec_outer.contains(&task.uid()) {
4402            // A fork branch the activation does not hand its recursion to
4403            // (a virtual-task or detached branch): inlining would not end.
4404            return Err(LoweringError::unsupported(format!(
4405                "call to task `{}` from a branch of its own recursive activation that cannot \
4406                 spawn it (LRM 1800-2023 13.4.1)",
4407                task.name()
4408            )));
4409        }
4410        if self.inline_stack.contains(&task.uid()) {
4411            return Err(LoweringError::unsupported(format!(
4412                "recursive call to task `{}`, which contains timing controls (it has to be \
4413                 inlined into the calling process)",
4414                task.name()
4415            )));
4416        }
4417        if !self.rec_outer.contains(&task.uid())
4418            && task_reaches_itself(&task, self.virtual_tasks.as_deref())
4419        {
4420            return self.inline_recursive_task(actuals, task, cur, this);
4421        }
4422        let body = proc_body(&task).ok_or_else(|| {
4423            LoweringError::malformed(format!("task `{}` has no body", task.name()))
4424        })?;
4425        let formals: Vec<Symbol<'d>> = task.subroutine_arguments().collect();
4426        if formals.len() != actuals.len() {
4427            return Err(LoweringError::malformed(format!(
4428                "call to task `{}` has {} arguments for {} formals",
4429                task.name(),
4430                actuals.len(),
4431                formals.len()
4432            )));
4433        }
4434        let entry = self.alloc_block();
4435        self.close(cur, Terminator::Fallthrough(entry));
4436        let after = self.alloc_block();
4437        self.target_depth.insert(after, self.owned_scopes.len());
4438        let frame_mark = self.inlined_locals.len();
4439        let mut releases = Vec::new();
4440        if let Some((this_var, receiver)) = this {
4441            let binding = match receiver {
4442                Some(r) => ArgBinding::In {
4443                    formal: this_var,
4444                    actual: r,
4445                },
4446                None => ArgBinding::ThisOf { var: this_var },
4447            };
4448            self.blocks[entry.0 as usize].bindings.push(binding);
4449            self.inlined_locals.push(this_var);
4450            // Both bindings hold one count on the object for the call.
4451            releases.push(ArgBinding::Release { var: this_var });
4452        }
4453        let mut copy_backs = Vec::new();
4454        let mut aliases = Vec::new();
4455        let mut bound_refs: Vec<(Symbol<'d>, Expression<'d>)> = Vec::new();
4456        for (formal, &actual) in formals.iter().zip(actuals) {
4457            // slang binds an output/inout actual as `actual = <formal>`.
4458            let lvalue = if actual.kind() == ExpressionKind::Assignment {
4459                actual.left().ok_or_else(|| {
4460                    LoweringError::malformed("output argument with no target expression")
4461                })?
4462            } else {
4463                actual
4464            };
4465            // A `ref` naming static storage (directly or through an
4466            // enclosing call's aliased `ref`) is that storage in the body.
4467            // (An `event` (or event array) ref formal is a handle: copied in and back out, like
4468            // `inout`, so the body's `r = e` merges the actual's event.)
4469            if formal.formal_argument_direction() == sv_lang::ArgumentDirection::Ref
4470                && !formal
4471                    .value_type()
4472                    .as_ref()
4473                    .is_some_and(|t| crate::state::event_pool::event_count(t).is_some())
4474                && let Some(target) = self.ref_storage(lvalue)
4475            {
4476                self.ref_candidates.push((*formal, Some(target)));
4477                aliases.push((*formal, target));
4478                continue;
4479            }
4480            // A `ref` to a queue / dynamic-array element of a packed or real
4481            // type (LRM 1800-2023 13.5.2): the formal is a live alias, every
4482            // access of it goes through a runtime reference
4483            // ([`ArgBinding::RefOpen`]); other element types keep the
4484            // copy-in/copy-out of `inout`.
4485            if formal.formal_argument_direction() == sv_lang::ArgumentDirection::Ref
4486                && lvalue.kind() == ExpressionKind::ElementSelect
4487                && formal
4488                    .value_type()
4489                    .is_some_and(|t| t.is_integral() || t.is_floating())
4490                && lvalue
4491                    .select_value()
4492                    .and_then(|b| b.expr_type())
4493                    .is_some_and(|t| t.is_dynamically_sized_array() || t.is_associative_array())
4494            {
4495                // One reference-id slot per formal, shared by every call
4496                // site: the body reaches the reference through the formal.
4497                let prior =
4498                    self.blocks
4499                        .iter()
4500                        .flat_map(|b| b.bindings.iter())
4501                        .find_map(|b| match b {
4502                            ArgBinding::RefOpen {
4503                                temp, formal: f, ..
4504                            } if f.uid() == formal.uid() => Some(*temp),
4505                            _ => None,
4506                        });
4507                let temp = prior.unwrap_or_else(|| {
4508                    self.captures.push(lvalue);
4509                    self.captures.len() as u32 - 1
4510                });
4511                self.blocks[entry.0 as usize]
4512                    .bindings
4513                    .push(ArgBinding::RefOpen {
4514                        temp,
4515                        formal: *formal,
4516                        actual: lvalue,
4517                    });
4518                copy_backs.push(ArgBinding::RefClose {
4519                    temp,
4520                    formal: *formal,
4521                    actual: lvalue,
4522                });
4523                self.ref_candidates.push((*formal, None));
4524                if !crate::netlist::is_static_procedural_var(formal) {
4525                    self.inlined_locals.push(*formal);
4526                }
4527                // 37.15 detail 3: the formal is a ref obj of the task's
4528                // frame, bound to the element the call named.
4529                bound_refs.push((*formal, lvalue));
4530                continue;
4531            }
4532            let binding = match formal.formal_argument_direction() {
4533                sv_lang::ArgumentDirection::In => ArgBinding::In {
4534                    formal: *formal,
4535                    actual,
4536                },
4537                sv_lang::ArgumentDirection::Out => ArgBinding::Init { var: *formal },
4538                sv_lang::ArgumentDirection::InOut | sv_lang::ArgumentDirection::Ref => {
4539                    ArgBinding::In {
4540                        formal: *formal,
4541                        actual: lvalue,
4542                    }
4543                }
4544            };
4545            self.blocks[entry.0 as usize].bindings.push(binding);
4546            if formal.formal_argument_direction() == sv_lang::ArgumentDirection::Ref {
4547                bound_refs.push((*formal, lvalue));
4548                let named = (crate::symid::is_value_name(lvalue.kind())
4549                    && !formal
4550                        .value_type()
4551                        .as_ref()
4552                        .is_some_and(|t| crate::state::event_pool::event_count(t).is_some()))
4553                .then(|| lvalue.referenced_symbol())
4554                .flatten();
4555                self.ref_candidates.push((*formal, named));
4556            }
4557            if formal.formal_argument_direction() != sv_lang::ArgumentDirection::In {
4558                copy_backs.push(ArgBinding::Out {
4559                    actual: lvalue,
4560                    formal: *formal,
4561                });
4562            }
4563            // A static task's formal is the instance's (a signal).
4564            if !crate::netlist::is_static_procedural_var(formal) {
4565                self.inlined_locals.push(*formal);
4566                if formal.formal_argument_direction() == sv_lang::ArgumentDirection::In {
4567                    releases.push(ArgBinding::Release { var: *formal });
4568                }
4569            }
4570        }
4571        // A method's body (and its locals' initializers) runs against its
4572        // own `this`, in blocks of its own; the bindings above ran in the
4573        // caller's.
4574        if let Some((this_var, _)) = this {
4575            self.this_stack.push(this_var);
4576        }
4577        let n_aliases = aliases.len();
4578        self.ref_stack.extend(aliases);
4579        let body_entry = if this.is_some() || n_aliases > 0 {
4580            let b = self.alloc_block();
4581            self.close(entry, Terminator::Fallthrough(b));
4582            b
4583        } else {
4584            entry
4585        };
4586        for local in task_locals(&task) {
4587            // A method's `this` is bound above, never a default-initialized
4588            // local.
4589            if this.is_some_and(|(t, _)| t.uid() == local.uid()) {
4590                continue;
4591            }
4592            // A static variable is the instance's (a signal); a block's
4593            // automatic variables are initialized on entry to it.
4594            if crate::netlist::is_static_procedural_var(&local) {
4595                continue;
4596            }
4597            if !is_automatic_block_var(&local) {
4598                self.blocks[body_entry.0 as usize]
4599                    .bindings
4600                    .push(ArgBinding::Init { var: local });
4601                releases.push(ArgBinding::Release { var: local });
4602            }
4603            self.inlined_locals.push(local);
4604        }
4605        // An inlined value-returning function (a `fork` in its body cannot be
4606        // compiled, LRM 13.4.4): its result variable is a local of the
4607        // process, which `return expr;` / `f = expr;` assign and the hoisted
4608        // call's expression reads (`hoist_suspending_function_calls`).
4609        let ret_var = if task.subroutine_kind() == Some(sv_lang::SubroutineKind::Function)
4610            && task.subroutine_return_type().is_some_and(|t| !t.is_void())
4611        {
4612            task.subroutine_return_val_var()
4613        } else {
4614            None
4615        };
4616        if let Some(rv) = ret_var {
4617            self.blocks[body_entry.0 as usize]
4618                .bindings
4619                .push(ArgBinding::Init { var: rv });
4620            self.inlined_locals.push(rv);
4621        }
4622        // The pattern variables of the body's `matches` (LRM 12.6).
4623        for v in crate::patterns::pattern_vars(body.as_sem_node()) {
4624            if !self.inlined_locals.iter().any(|l| l.uid() == v.uid()) {
4625                self.inlined_locals.push(v);
4626            }
4627        }
4628        // VPI frame (37.43): the formals and locals this inlining made
4629        // automatic variables of the process, in that order.
4630        let frame_vars: Vec<Symbol<'d>> = self.inlined_locals[frame_mark..]
4631            .iter()
4632            .copied()
4633            .filter(|v| this.is_none_or(|(t, _)| t.uid() != v.uid()))
4634            .filter(|v| !bound_refs.iter().any(|(f, _)| f.uid() == v.uid()))
4635            .collect();
4636        self.frame_probe(
4637            body_entry,
4638            FrameProbe::Enter {
4639                task,
4640                vars: frame_vars.clone(),
4641                bound: bound_refs,
4642            },
4643        );
4644        self.block_event(body_entry, task, false);
4645        self.event_stack.push((task, self.disable_targets.len()));
4646        self.frame_stack.push((task, frame_vars.clone()));
4647        self.inline_stack.push(task.uid());
4648        self.return_targets.push(after);
4649        self.ret_var_stack.push(ret_var);
4650        // `disable task;` ends this activation (LRM 9.6.2), from inside it or
4651        // from another process: the exit is the block after the call, whose
4652        // frame probe ends the activation's VPI frame.
4653        let task_name = task.name().to_string();
4654        let task_scoped = is_disabled_somewhere(&task_name);
4655        let mut n_tags = 0;
4656        if task_scoped {
4657            self.scope_chain
4658                .push((task_name.clone(), after, self.frame_stack.len()));
4659            self.disable_targets
4660                .push((task_name, after, self.frame_stack.len()));
4661            // LRM 25.7.4: an `extern` method's activation is also the scope
4662            // `disable m1.a.t;` (this module instance's call) and `disable
4663            // b.t;` (every call through the interface instance) name.
4664            for tag in crate::netlist::itf::extern_scope_tags(&task) {
4665                self.scope_chain.push((tag, after, self.frame_stack.len()));
4666                n_tags += 1;
4667            }
4668        }
4669        let exit = self.walk_stmt(body, body_entry);
4670        if task_scoped {
4671            self.scope_chain.pop();
4672            self.disable_targets.pop();
4673            for _ in 0..n_tags {
4674                self.scope_chain.pop();
4675            }
4676        }
4677        self.event_stack.pop();
4678        self.return_targets.pop();
4679        self.ret_var_stack.pop();
4680        self.inline_stack.pop();
4681        self.frame_stack.pop();
4682        if this.is_some() {
4683            self.this_stack.pop();
4684        }
4685        self.ref_stack.truncate(self.ref_stack.len() - n_aliases);
4686        let exit = exit?;
4687        self.close(exit, Terminator::Fallthrough(after));
4688        self.blocks[after.0 as usize].bindings.extend(copy_backs);
4689        self.blocks[after.0 as usize].bindings.extend(releases);
4690        self.block_event(after, task, true);
4691        self.frame_probe(
4692            after,
4693            FrameProbe::Exit {
4694                task,
4695                vars: frame_vars,
4696            },
4697        );
4698        Ok(after)
4699    }
4700
4701    /// An immediate assertion whose pass or fail statement waits (LRM 16.3):
4702    /// [`Terminator::Assertion`] over the two statements' own blocks.
4703    fn walk_immediate_assertion(
4704        &mut self,
4705        stmt: Statement<'d>,
4706        (pass_stmt, fail_stmt): (Option<Statement<'d>>, Option<Statement<'d>>),
4707        cur: BlockId,
4708    ) -> Result<BlockId, LoweringError> {
4709        if stmt.is_deferred_assertion() == Some(true) {
4710            return Err(LoweringError::unsupported(
4711                "a deferred assertion (LRM 16.4) whose action block waits (the action must be \
4712                 a subroutine call that is queued)"
4713                    .to_string(),
4714            ));
4715        }
4716        let pass_blk = self.alloc_block();
4717        let fail_blk = self.alloc_block();
4718        let done_blk = self.alloc_block();
4719        self.close(
4720            cur,
4721            Terminator::Assertion {
4722                stmt,
4723                pass_blk,
4724                fail_blk,
4725                done_blk,
4726            },
4727        );
4728        for (action, blk) in [(pass_stmt, pass_blk), (fail_stmt, fail_blk)] {
4729            let exit = match action {
4730                Some(a) => self.walk_stmt(a, blk)?,
4731                None => blk,
4732            };
4733            self.close(exit, Terminator::Fallthrough(done_blk));
4734        }
4735        Ok(done_blk)
4736    }
4737
4738    /// A `case` whose arms need their own blocks ([`Terminator::Case`]).
4739    fn walk_case(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
4740        // A `case ... matches` has its own item and default accessors.
4741        let is_pattern = stmt.kind() == StatementKind::PatternCase;
4742        // A constant selector over arms one of which slang demoted to
4743        // `Invalid` (a statically dead arm): only the matching arm can run
4744        // (LRM 12.5), so lower just it.
4745        if !is_pattern && let Some(live) = constant_case_live_arm(&stmt) {
4746            return match live {
4747                Some(arm) => self.walk_stmt(arm, cur),
4748                None => Ok(cur),
4749            };
4750        }
4751        let arm_stmts: Vec<Statement<'d>> = if is_pattern {
4752            stmt.pattern_case_items().iter().map(|i| i.stmt()).collect()
4753        } else {
4754            stmt.case_items().iter().map(|i| i.stmt()).collect()
4755        };
4756        let arms: Vec<BlockId> = arm_stmts.iter().map(|_| self.alloc_block()).collect();
4757        let default_blk = self.alloc_block();
4758        let join = self.alloc_block();
4759        self.close(
4760            cur,
4761            Terminator::Case {
4762                stmt,
4763                arms: arms.clone(),
4764                default_blk,
4765            },
4766        );
4767        for (arm_stmt, &arm) in arm_stmts.iter().zip(&arms) {
4768            let exit = self.walk_stmt(*arm_stmt, arm)?;
4769            self.close(exit, Terminator::Fallthrough(join));
4770        }
4771        let default_stmt = if is_pattern {
4772            stmt.pattern_case_default()
4773        } else {
4774            stmt.default_case()
4775        };
4776        let default_exit = match default_stmt {
4777            Some(d) => self.walk_stmt(d, default_blk)?,
4778            None => default_blk,
4779        };
4780        self.close(default_exit, Terminator::Fallthrough(join));
4781        Ok(join)
4782    }
4783
4784    /// Walks a loop body with `continue` going to `cont` and `break` to
4785    /// `brk`.
4786    fn walk_loop_body(
4787        &mut self,
4788        body_stmt: Statement<'d>,
4789        body: BlockId,
4790        cont: BlockId,
4791        brk: BlockId,
4792    ) -> Result<BlockId, LoweringError> {
4793        self.loop_targets.push((cont, brk));
4794        let open = self.owned_scopes.len();
4795        self.target_depth.insert(cont, open);
4796        self.target_depth.insert(brk, open);
4797        let exit = self.walk_stmt(body_stmt, body);
4798        self.loop_targets.pop();
4799        exit
4800    }
4801
4802    /// Walks a block, keeping [`Self::name_path`] current for the sequential
4803    /// block's own name.
4804    fn walk_block(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
4805        // `L: assert (..) ... disable L; ...`: one leaf (see the helper).
4806        if !self.entering_block && self_disabling_assertion_block(&stmt) {
4807            self.push(cur, stmt);
4808            return Ok(cur);
4809        }
4810        let name = (!self.entering_block
4811            && stmt.block_kind() == Some(StatementBlockKind::Sequential))
4812        .then(|| block_name(&stmt))
4813        .filter(|n| !n.is_empty());
4814        let pushed = name.is_some();
4815        if let Some(n) = name {
4816            self.name_path.push(n);
4817        }
4818        // LRM 19.3: `@@(begin blk)` / `@@(end blk)` of a named block, signalled
4819        // at its first statement and after its last (the codegen drops the
4820        // ones no covergroup names). Only a block that is walked into the CFG
4821        // gets here; a leaf block's events are emitted where it is lowered.
4822        let blk = (!self.entering_block
4823            && stmt.block_kind() == Some(StatementBlockKind::Sequential))
4824        .then(|| stmt.block_symbol())
4825        .flatten()
4826        .filter(|b| !b.name().is_empty());
4827        if let Some(b) = blk {
4828            self.block_event(cur, b, false);
4829            self.event_stack.push((b, self.disable_targets.len()));
4830        }
4831        let r = self.walk_block_inner(stmt, cur);
4832        if blk.is_some() {
4833            self.event_stack.pop();
4834        }
4835        if pushed {
4836            self.name_path.pop();
4837        }
4838        if let (Some(b), Ok(end)) = (blk, &r) {
4839            self.block_event(*end, b, true);
4840        }
4841        r
4842    }
4843
4844    fn walk_block_inner(
4845        &mut self,
4846        stmt: Statement<'d>,
4847        cur: BlockId,
4848    ) -> Result<BlockId, LoweringError> {
4849        // `StatementBlockKind` is a plain (non-`#[non_exhaustive]`) enum
4850        // hand-authored in `sv-lang`, so this literal `match` is genuinely,
4851        // compiler-checked exhaustive -- no wildcard needed or used.
4852        match stmt.block_kind() {
4853            // A named block some `disable` inside it can leave: its own exit.
4854            // A block declaring automatic variables: they are created and
4855            // initialized on each entry (LRM 1800-2023 §6.21), at the
4856            // start of a CFG block of its own.
4857            Some(_) if !self.entering_block && !block_automatic_vars(&stmt, true).is_empty() => {
4858                // A CFG block with no statements yet takes the bindings
4859                // itself (they run before its statements).
4860                let fresh = &self.blocks[cur.0 as usize];
4861                let entry = if fresh.stmts.is_empty() && fresh.step_exprs.is_empty() {
4862                    cur
4863                } else {
4864                    let entry = self.alloc_block();
4865                    self.close(cur, Terminator::Fallthrough(entry));
4866                    entry
4867                };
4868                for var in block_automatic_vars(&stmt, true) {
4869                    self.blocks[entry.0 as usize]
4870                        .bindings
4871                        .push(ArgBinding::Init { var });
4872                }
4873                // A sequential block's handle-holding variables end with it.
4874                let owned: Vec<Symbol<'d>> =
4875                    if stmt.block_kind() == Some(StatementBlockKind::Sequential) {
4876                        block_automatic_vars(&stmt, true)
4877                            .into_iter()
4878                            .filter(|v| v.value_type().is_some_and(|t| may_hold_handles(&t)))
4879                            .collect()
4880                    } else {
4881                        Vec::new()
4882                    };
4883                let scoped = !owned.is_empty();
4884                if scoped {
4885                    self.owned_scopes.push(owned.clone());
4886                }
4887                self.entering_block = true;
4888                let r = self.walk_block(stmt, entry);
4889                self.entering_block = false;
4890                if !scoped {
4891                    return r;
4892                }
4893                self.owned_scopes.pop();
4894                let end = r?;
4895                let leave = self.alloc_block();
4896                self.close(end, Terminator::Fallthrough(leave));
4897                for var in owned.into_iter().rev() {
4898                    self.blocks[leave.0 as usize]
4899                        .bindings
4900                        .push(ArgBinding::Release { var });
4901                }
4902                Ok(leave)
4903            }
4904            Some(StatementBlockKind::Sequential)
4905                if let name = block_name(&stmt)
4906                    && !name.is_empty()
4907                    && (is_disabled_somewhere(&name)
4908                        || is_disabled_somewhere(&self.name_path.join("."))
4909                        || contains_disable_through_calls(stmt.as_sem_node())) =>
4910            {
4911                self.entering_block = false;
4912                let exit = self.alloc_block();
4913                self.target_depth.insert(exit, self.owned_scopes.len());
4914                // LRM 9.6.2: another process's `disable` of this block resumes
4915                // the process at `exit`, wherever in the block it is suspended.
4916                let scoped = is_disabled_somewhere(&name);
4917                if scoped {
4918                    self.scope_chain
4919                        .push((name.clone(), exit, self.frame_stack.len()));
4920                }
4921                // `disable p.b.x;` of another process names the block by its
4922                // path from the process's outermost block: a second scope
4923                // under that dotted label tells same-named blocks apart.
4924                let dotted = self.name_path.join(".");
4925                let path_scoped = dotted.contains('.') && is_disabled_somewhere(&dotted);
4926                if path_scoped {
4927                    self.scope_chain
4928                        .push((dotted.clone(), exit, self.frame_stack.len()));
4929                }
4930                // `disable fl.b1.blk;` goes through the fork's own label `fl`
4931                // (9.6.2): the branch's path under that label tells the
4932                // same-named blocks of sibling branches apart.
4933                let fork_key = self
4934                    .fork_name
4935                    .as_ref()
4936                    .map(|f| format!("{f}.{dotted}"))
4937                    .filter(|k| is_disabled_somewhere(k));
4938                let fork_scoped = fork_key.is_some();
4939                if let Some(key) = fork_key {
4940                    self.scope_chain.push((key, exit, self.frame_stack.len()));
4941                }
4942                self.disable_targets
4943                    .push((name, exit, self.frame_stack.len()));
4944                let end = self.walk_seq(live_statements(&stmt), cur);
4945                self.disable_targets.pop();
4946                if fork_scoped {
4947                    self.scope_chain.pop();
4948                }
4949                if path_scoped {
4950                    self.scope_chain.pop();
4951                }
4952                if scoped {
4953                    self.scope_chain.pop();
4954                }
4955                self.close(end?, Terminator::Fallthrough(exit));
4956                Ok(exit)
4957            }
4958            Some(StatementBlockKind::Sequential) => {
4959                self.entering_block = false;
4960                self.walk_seq(live_statements(&stmt), cur)
4961            }
4962            Some(
4963                kind @ (StatementBlockKind::JoinAll
4964                | StatementBlockKind::JoinAny
4965                | StatementBlockKind::JoinNone),
4966            ) => {
4967                // Task 1 (PR-069/`docs/design/process.md` §7): construction
4968                // only, never scheduled. Each branch is walked as its own,
4969                // fully independent `ProcessCfg` (`OneShot` -- a fork
4970                // branch runs to completion or suspends, but never
4971                // re-triggers the way an `always` process does).
4972                //
4973                // Verified live: `>1` un-`begin`/`end`-wrapped branches
4974                // (`fork a; b; join`) are NOT `stmt.statements()`'s direct
4975                // elements -- slang groups them into one `List` node first
4976                // (the same generic multi-statement wrapper `walk_stmt`'s
4977                // own `StatementKind::List` arm already flattens elsewhere),
4978                // so `statements()` here returns exactly one `List` element,
4979                // not one element per branch. A single, un-wrapped branch
4980                // (`fork a; join`) has no such wrapper. Flatten exactly one
4981                // `List` level when present, never more (a `List`
4982                // containing a nested `List` is not a shape any fixture
4983                // this task constructs, so silently over-flattening would
4984                // risk merging two real branches into one).
4985                let branch_stmts = live_statements(&stmt);
4986                let branch_stmts = match branch_stmts.as_slice() {
4987                    [only] if only.kind() == StatementKind::List => live_statements(only),
4988                    _ => branch_stmts,
4989                };
4990                self.entering_block = false;
4991                let fork_name = Some(block_name(&stmt)).filter(|n| !n.is_empty());
4992                // LRM 9.6.2: a `disable` of one branch's own named outermost
4993                // block, reachable from the parent (after this fork
4994                // statement), a sibling branch, or deeper still (threaded
4995                // into each branch's own `Inherited::known_fork_labels`
4996                // below) -- see `CfgBuilder::fork_branch_names`'s own doc
4997                // comment. Computed from a reference: `branch_stmts` is
4998                // still needed, un-consumed, by the `.into_iter()` build
4999                // below.
5000                // The fork block's own name (`fork : NAME`) tags every branch
5001                // process too (`ProcessCfg::disable_names`), so a later
5002                // `disable NAME;` of the parent, after the join, ends the
5003                // branches still running (LRM 9.6.2: all activities enabled
5004                // within the named block end).
5005                let new_branch_names: Vec<String> = branch_stmts
5006                    .iter()
5007                    .filter_map(|s| peer_block(s).map(|p| p.name))
5008                    .collect();
5009                let known_fork_labels: Vec<String> = self
5010                    .fork_branch_names
5011                    .iter()
5012                    .filter(|n| !self.fork_block_names.contains(n))
5013                    .cloned()
5014                    .chain(new_branch_names.iter().cloned())
5015                    .collect();
5016                let ancestor_blocks: Vec<String> = self
5017                    .ancestor_blocks
5018                    .iter()
5019                    .cloned()
5020                    .chain(self.disable_targets.iter().map(|(n, _, _)| n.clone()))
5021                    .collect();
5022                // Equal inherited context and statement give an equal CFG
5023                // (a fresh builder is seeded from `Inherited` alone), so a
5024                // task inlined at many call sites builds each of its forks'
5025                // branches once and the lowering compiles each once
5026                // ([`ProcessCfg::share_id`]). Not inside a recursive
5027                // activation (its capture temporaries are per site) or
5028                // under a clock inference.
5029                let memoizable = self.rec.is_none()
5030                    && self.rec_up.is_empty()
5031                    && self.global_clock.is_none()
5032                    && self.checker_clock.is_none();
5033                let mut branches: Vec<ProcessCfg<'d>> = branch_stmts
5034                    .into_iter()
5035                    .map(|branch_stmt| {
5036                        let key = memoizable.then(|| {
5037                            MemoKey::Fork(ForkBranchKey {
5038                                stmt: (stmt_addr(&branch_stmt), branch_stmt.kind() as u16),
5039                                fork_name: fork_name.clone(),
5040                                default_clocking: self.default_clocking.map(|c| c.uid()),
5041                                this_var: self.this_stack.last().map(|t| t.uid()),
5042                                ref_aliases: self
5043                                    .ref_stack
5044                                    .iter()
5045                                    .map(|(a, b)| (a.uid(), b.uid()))
5046                                    .collect(),
5047                                rec_outer: self.rec_outer.clone(),
5048                                labels: known_fork_labels.clone(),
5049                                ancestors: ancestor_blocks.clone(),
5050                            })
5051                        });
5052                        if let Some(k) = &key
5053                            && let Some(hit) = self.fork_memo.borrow().get(k)
5054                        {
5055                            return Ok(hit.clone());
5056                        }
5057                        let mut cfg = build_cfg_in(
5058                            &branch_stmt,
5059                            ProcessLoopKind::OneShot,
5060                            fork_name.clone(),
5061                            self.default_clocking,
5062                            None,
5063                            self.checker_clock,
5064                            Inherited {
5065                                this_var: self.this_stack.last().copied(),
5066                                ref_aliases: self.ref_stack.clone(),
5067                                global_clock: self.global_clock,
5068                                detached_trigger: false,
5069                                forced_extern: None,
5070                                rec_outer: self.rec_outer.clone(),
5071                                fork_memo: Some(self.fork_memo.clone()),
5072                                rec: self.rec.clone(),
5073                                rec_up: self.rec_up.clone(),
5074                                known_fork_labels: known_fork_labels.clone(),
5075                                ancestor_blocks: ancestor_blocks.clone(),
5076                            },
5077                            self.virtual_tasks.clone(),
5078                        )?;
5079                        if let Some(k) = key {
5080                            cfg.share_id = Some(
5081                                NEXT_SHARE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
5082                            );
5083                            self.fork_memo.borrow_mut().insert(k, cfg.clone());
5084                        }
5085                        Ok(cfg)
5086                    })
5087                    .collect::<Result<_, LoweringError>>()?;
5088                for b in &mut branches {
5089                    b.captured = captured_vars(b);
5090                    // An aliased `ref` formal is the branch's storage too.
5091                    b.captured
5092                        .retain(|c| !self.ref_stack.iter().any(|(f, _)| f.uid() == c.uid()));
5093                    // A branch inside an inlined class-method task shares the
5094                    // forking activation's `this`.
5095                    if let Some(t) = self.this_stack.last()
5096                        && !b.captured.iter().any(|c| c.uid() == t.uid())
5097                    {
5098                        b.captured.push(*t);
5099                    }
5100                }
5101                // The parent keeps these labels resolvable for the rest of
5102                // its own walk (not popped like `disable_targets`): LRM
5103                // 9.6.2 lets a branch be disabled for the rest of the
5104                // process's life, not only while this `fork` statement is
5105                // still being walked.
5106                self.fork_branch_names.extend(new_branch_names);
5107                if let Some(f) = &fork_name {
5108                    self.fork_branch_names.push(f.clone());
5109                    self.fork_block_names.push(f.clone());
5110                }
5111                let join_blk = self.alloc_block();
5112                self.close(
5113                    cur,
5114                    Terminator::ForkJoin {
5115                        kind,
5116                        branches,
5117                        join_blk,
5118                    },
5119                );
5120                Ok(join_blk)
5121            }
5122            None => Err(LoweringError::malformed(
5123                "Block statement with no block_kind (unexpected AST shape)",
5124            )),
5125        }
5126    }
5127
5128    fn walk_seq(
5129        &mut self,
5130        stmts: Vec<Statement<'d>>,
5131        mut cur: BlockId,
5132    ) -> Result<BlockId, LoweringError> {
5133        // LRM 14.11: a `##0` whose clocking event already occurred in this
5134        // time step "shall have no effect". Statically known exactly when the
5135        // process was just released by that event (`##N;` or
5136        // `@(cb);` on the default clocking, a `##0` included once it has waited) and only untimed, call-free
5137        // statements ran since; any other `##0` waits for the event (which is
5138        // right whenever the event has not yet fired this step).
5139        let mut released = false;
5140        for s in stmts {
5141            let s = unwrap_invalid(s);
5142            match self.default_clocking_wait(&s) {
5143                Some(0) if released => {
5144                    if let Some(body) = s.body()
5145                        && body.kind() != StatementKind::Empty
5146                    {
5147                        cur = self.walk_stmt(body, cur)?;
5148                    }
5149                    continue;
5150                }
5151                Some(_) => released = s.body().is_none_or(|b| b.kind() == StatementKind::Empty),
5152                None => {
5153                    released = released && Self::untimed_simple(&s);
5154                }
5155            }
5156            cur = self.walk_stmt(s, cur)?;
5157        }
5158        Ok(cur)
5159    }
5160
5161    /// The cycle count of `##N` / `@(cb)` (count 1) on the default clocking
5162    /// when `stmt` is such a timed statement (see [`Self::walk_seq`]).
5163    fn default_clocking_wait(&self, stmt: &Statement<'d>) -> Option<u32> {
5164        let cb = self.default_clocking?;
5165        if stmt.kind() != StatementKind::Timed {
5166            return None;
5167        }
5168        let tc = stmt.timing_control()?;
5169        // A predicate, not a lowering: any other timing control is simply not
5170        // a wait on the default clocking (the next `##0` then waits).
5171        if tc.kind() == TimingControlKind::CycleDelay {
5172            first_expression_child(&tc)?
5173                .constant_value()?
5174                .as_i64()
5175                .and_then(|v| u32::try_from(v).ok())
5176        } else if tc.kind() == TimingControlKind::SignalEvent && tc.edge() == EdgeKind::None {
5177            let sym = first_expression_child(&tc)?.referenced_symbol()?;
5178            (sym.kind() == SymbolKind::ClockingBlock && sym.uid() == cb.uid()).then_some(1)
5179        } else {
5180            None
5181        }
5182    }
5183
5184    /// A statement that takes no time and calls no user subroutine: a system
5185    /// call or an untimed assignment.
5186    fn untimed_simple(stmt: &Statement<'d>) -> bool {
5187        stmt.kind() == StatementKind::ExpressionStatement
5188            && stmt.expr().is_some_and(|e| {
5189                (e.kind() == ExpressionKind::Call && e.is_system_call() == Some(true))
5190                    || (e.kind() == ExpressionKind::Assignment && e.assignment_timing().is_none())
5191            })
5192    }
5193
5194    /// PR-044: `if`/`else` always expands to `Terminator::Branch`, regardless
5195    /// of whether either branch contains a suspend -- this is orthogonal to
5196    /// (never triggers) suspend splitting, which is exactly what
5197    /// `if_else_does_not_split_the_cfg` (`tests/process_cfg.rs`) proves: the
5198    /// number of *additional* blocks a wrapping suspend contributes is the
5199    /// same with or without the branch present.
5200    fn walk_conditional(
5201        &mut self,
5202        stmt: Statement<'d>,
5203        cur: BlockId,
5204    ) -> Result<BlockId, LoweringError> {
5205        if violation_check(&stmt).is_some() || is_clause_if(&stmt) {
5206            return self.walk_checked_if(stmt, cur);
5207        }
5208        let cond = stmt.expressions().into_iter().next().ok_or_else(|| {
5209            LoweringError::malformed("Conditional statement with no condition expression")
5210        })?;
5211        let then_stmt = stmt
5212            .then_branch()
5213            .ok_or_else(|| LoweringError::malformed("Conditional statement with no then-branch"))?;
5214        let else_stmt = stmt.else_branch();
5215
5216        // A constant predicate picks its arm at elaboration; only the live
5217        // arm becomes blocks (see `constant_if_arm`).
5218        if let Some(taken) = constant_if_arm(&stmt) {
5219            return match if taken { Some(then_stmt) } else { else_stmt } {
5220                Some(live) => self.walk_stmt(live, cur),
5221                None => Ok(cur),
5222            };
5223        }
5224
5225        // A call in the condition to a function that is inlined (a `fork` in
5226        // its body, an event output formal) runs ahead of the test; the
5227        // condition reads its result variable.
5228        let cur = self
5229            .hoist_suspending_calls_in(cond.as_sem_node(), cur)?
5230            .unwrap_or(cur);
5231        let then_blk = self.alloc_block();
5232        let else_blk = self.alloc_block();
5233        self.close(
5234            cur,
5235            Terminator::Branch {
5236                cond,
5237                then_blk,
5238                else_blk,
5239            },
5240        );
5241
5242        let then_exit = self.walk_stmt(then_stmt, then_blk)?;
5243        let else_exit = match else_stmt {
5244            Some(e) => self.walk_stmt(e, else_blk)?,
5245            None => else_blk,
5246        };
5247
5248        let join = self.alloc_block();
5249        self.close(then_exit, Terminator::Fallthrough(join));
5250        self.close(else_exit, Terminator::Fallthrough(join));
5251        Ok(join)
5252    }
5253
5254    /// A `unique`/`unique0`/`priority` `if` series (§12.4.2): one
5255    /// [`Terminator::Case`] over the whole series, `arms[i]` the then-statement
5256    /// of its i-th condition, `default_blk` the final `else` (empty without
5257    /// one), so the dispatch can report a violation (§12.4.2.1) where the
5258    /// series executes.
5259    fn walk_checked_if(
5260        &mut self,
5261        stmt: Statement<'d>,
5262        cur: BlockId,
5263    ) -> Result<BlockId, LoweringError> {
5264        let chain = if_series(stmt).map_err(|why| {
5265            LoweringError::unsupported(format!("{why} in an if series (malformed statement)"))
5266        })?;
5267        let arms: Vec<BlockId> = chain.thens.iter().map(|_| self.alloc_block()).collect();
5268        let default_blk = self.alloc_block();
5269        let join = self.alloc_block();
5270        self.close(
5271            cur,
5272            Terminator::Case {
5273                stmt,
5274                arms: arms.clone(),
5275                default_blk,
5276            },
5277        );
5278        for (then_stmt, &arm) in chain.thens.iter().zip(&arms) {
5279            let exit = self.walk_stmt(*then_stmt, arm)?;
5280            self.close(exit, Terminator::Fallthrough(join));
5281        }
5282        let default_exit = match chain.otherwise {
5283            Some(e) => self.walk_stmt(e, default_blk)?,
5284            None => default_blk,
5285        };
5286        self.close(default_exit, Terminator::Fallthrough(join));
5287        Ok(join)
5288    }
5289
5290    /// PR-006/PR-007: the one place a `BasicBlock` closes with
5291    /// `Terminator::Suspend` from a `Timed` statement. `Delay`/`SignalEvent`
5292    /// (posedge/negedge only) are the slice-scope half; Task 1 adds
5293    /// `EventList` (as `SuspendPoint::EdgeList`) and `CycleDelay`. **Task 6
5294    /// (CG-081/CG-171)** adds real handling for `ImplicitEvent` (as
5295    /// `SuspendPoint::ImplicitComb`) and `OneStepDelay` (as `SuspendPoint::
5296    /// OneStep`), and revises `CycleDelay` to carry its inferred clock
5297    /// signal/edge as real payload instead of discarding it. `Delay3` (not
5298    /// applicable to a procedural suspend point at all), `BlockEventList` (a
5299    /// verified, hard upstream `sv-lang`=0.1.9 API gap) and `RepeatedEvent`
5300    /// (verified structurally unreachable at this dispatch point -- see that
5301    /// arm below) remain named [`LoweringError`]s.
5302    /// See [`Self::walk_stmt`]'s doc comment: `TimingControlKind` is also
5303    /// `#[non_exhaustive]` upstream, so this dispatch is likewise an
5304    /// `if`/`else-if` chain (pinned by
5305    /// `determinism::kind_guard::timing_control_kind_count_matches_pinned_
5306    /// sv_lang_version`, `TimingControlKind::COUNT == 10`), not a literal
5307    /// `match`.
5308    fn walk_timed(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
5309        let tc = stmt
5310            .timing_control()
5311            .ok_or_else(|| LoweringError::malformed("Timed statement with no timing control"))?;
5312        if !class_event_wait_terms(&tc)?.is_empty() && class_event_wait_is_mixed(&tc) {
5313            // `@(ce or sig ...)` (LRM 1800-2023 9.4.2.1, 15.5): the process
5314            // waits on a class event (a runtime queue) and on signals (the
5315            // sensitivity table) at once. The leaf statement queues it on the
5316            // class events without blocking, the suspend is the signal
5317            // wait, and whichever wakes it first wins: the resume block leaves
5318            // the class queues (`ClassEventLeave`), and a class trigger
5319            // desensitises the signal side (`Engine::apply_sync_wake`).
5320            let guarded = stmt
5321                .body()
5322                .ok_or_else(|| LoweringError::malformed("Timed statement with no guarded body"))?;
5323            self.skip_class_event_terms = true;
5324            let point = self.suspend_point(tc, None);
5325            self.skip_class_event_terms = false;
5326            let point = point?;
5327            let term_iffs = class_event_term_iffs(&tc);
5328            if term_iffs.iter().any(Option::is_some) {
5329                // A class term with an `iff`: when a class term woke the
5330                // process (a signal term's own `iff` was judged by the
5331                // sensitivity table), the first fired class term whose `iff`
5332                // holds continues it, and none waits on the whole list again
5333                // (LRM 9.4.2.3, each term judged on its own).
5334                if term_iffs.len() > 64 {
5335                    return Err(LoweringError::unsupported(
5336                        "an `@(a or b ...)` list of more than 64 class `event` terms with an `iff`",
5337                    ));
5338                }
5339                let header = self.alloc_block();
5340                let body = self.alloc_block();
5341                let exit = self.alloc_block();
5342                let resume = self.alloc_block();
5343                self.blocks[resume.0 as usize]
5344                    .bindings
5345                    .push(ArgBinding::ClassEventLeave);
5346                self.close(
5347                    cur,
5348                    Terminator::Loop {
5349                        header,
5350                        body,
5351                        exit,
5352                        entry: header,
5353                    },
5354                );
5355                self.close(header, Terminator::Fallthrough(body));
5356                self.push(body, stmt);
5357                self.close(
5358                    body,
5359                    Terminator::Suspend {
5360                        point,
5361                        resume_blk: resume,
5362                    },
5363                );
5364                let first = self.alloc_block();
5365                self.close(
5366                    resume,
5367                    Terminator::Switch {
5368                        on: SwitchOn::ClassFired(None),
5369                        arms: vec![exit, first],
5370                        default_blk: exit,
5371                        report: None,
5372                    },
5373                );
5374                let mut at = first;
5375                for (k, iff) in term_iffs.iter().enumerate() {
5376                    let chk = self.alloc_block();
5377                    let next = if k + 1 == term_iffs.len() {
5378                        header
5379                    } else {
5380                        self.alloc_block()
5381                    };
5382                    self.close(
5383                        at,
5384                        Terminator::Switch {
5385                            on: SwitchOn::ClassFired(Some(k as u32)),
5386                            arms: vec![next, chk],
5387                            default_blk: next,
5388                            report: None,
5389                        },
5390                    );
5391                    match iff {
5392                        Some(cond) => self.close(
5393                            chk,
5394                            Terminator::Branch {
5395                                cond: *cond,
5396                                then_blk: exit,
5397                                else_blk: next,
5398                            },
5399                        ),
5400                        None => self.close(chk, Terminator::Fallthrough(exit)),
5401                    }
5402                    at = next;
5403                }
5404                return self.walk_stmt(guarded, exit);
5405            }
5406            self.push(cur, stmt);
5407            let resume = self.alloc_block();
5408            self.blocks[resume.0 as usize]
5409                .bindings
5410                .push(ArgBinding::ClassEventLeave);
5411            self.close(
5412                cur,
5413                Terminator::Suspend {
5414                    point,
5415                    resume_blk: resume,
5416                },
5417            );
5418            return self.walk_stmt(guarded, resume);
5419        }
5420        if !class_event_wait_terms(&tc)?.is_empty() {
5421            // `@e` on a class event (LRM 1800-2023 15.5): the statement is the
5422            // leaf that queues the process on the event's runtime object; the
5423            // suspend after it lets the scheduler apply the block request,
5424            // like a blocking `semaphore.get`. Codegen lowers only the wait of
5425            // this leaf (the guarded body follows in the resumed block).
5426            let guarded = stmt
5427                .body()
5428                .ok_or_else(|| LoweringError::malformed("Timed statement with no guarded body"))?;
5429            let term_iffs = class_event_term_iffs(&tc);
5430            if term_iffs.iter().any(Option::is_some) {
5431                // `@(a iff x or b iff y ...)` of class events: the runtime
5432                // records which terms fired (`sv_rt_fired_mask`), the resume
5433                // takes the first fired term whose `iff` holds, and waits
5434                // again when none does (each term judged on its own, 9.4.2.3).
5435                if term_iffs.len() > 64 {
5436                    return Err(LoweringError::unsupported(
5437                        "an `@(a or b ...)` list of more than 64 class `event` terms with an `iff`",
5438                    ));
5439                }
5440                let header = self.alloc_block();
5441                let body = self.alloc_block();
5442                let exit = self.alloc_block();
5443                let resume = self.alloc_block();
5444                self.close(
5445                    cur,
5446                    Terminator::Loop {
5447                        header,
5448                        body,
5449                        exit,
5450                        entry: header,
5451                    },
5452                );
5453                self.close(header, Terminator::Fallthrough(body));
5454                self.push(body, stmt);
5455                self.close(
5456                    body,
5457                    Terminator::Suspend {
5458                        point: SuspendPoint::ByRequest,
5459                        resume_blk: resume,
5460                    },
5461                );
5462                let mut at = resume;
5463                for (k, iff) in term_iffs.iter().enumerate() {
5464                    let chk = self.alloc_block();
5465                    let next = if k + 1 == term_iffs.len() {
5466                        header
5467                    } else {
5468                        self.alloc_block()
5469                    };
5470                    self.close(
5471                        at,
5472                        Terminator::Switch {
5473                            on: SwitchOn::FiredBit(k as u32),
5474                            arms: vec![next, chk],
5475                            default_blk: next,
5476                            report: None,
5477                        },
5478                    );
5479                    match iff {
5480                        Some(cond) => self.close(
5481                            chk,
5482                            Terminator::Branch {
5483                                cond: *cond,
5484                                then_blk: exit,
5485                                else_blk: next,
5486                            },
5487                        ),
5488                        None => self.close(chk, Terminator::Fallthrough(exit)),
5489                    }
5490                    at = next;
5491                }
5492                return self.walk_stmt(guarded, exit);
5493            }
5494            if let Some(iff) = extract_iff(&tc) {
5495                // `@(e iff c)` (LRM 1800-2023 9.4.2.3): the event wakes the
5496                // process, which continues only if `c` holds then; otherwise
5497                // it waits for the event again. A loop around the plain wait.
5498                let header = self.alloc_block();
5499                let body = self.alloc_block();
5500                let exit = self.alloc_block();
5501                let resume = self.alloc_block();
5502                self.close(
5503                    cur,
5504                    Terminator::Loop {
5505                        header,
5506                        body,
5507                        exit,
5508                        entry: header,
5509                    },
5510                );
5511                self.close(header, Terminator::Fallthrough(body));
5512                self.push(body, stmt);
5513                self.close(
5514                    body,
5515                    Terminator::Suspend {
5516                        point: SuspendPoint::ByRequest,
5517                        resume_blk: resume,
5518                    },
5519                );
5520                self.close(
5521                    resume,
5522                    Terminator::Branch {
5523                        cond: iff,
5524                        then_blk: exit,
5525                        else_blk: header,
5526                    },
5527                );
5528                return self.walk_stmt(guarded, exit);
5529            }
5530            self.push(cur, stmt);
5531            let resume = self.alloc_block();
5532            self.close(
5533                cur,
5534                Terminator::Suspend {
5535                    point: SuspendPoint::ByRequest,
5536                    resume_blk: resume,
5537                },
5538            );
5539            return self.walk_stmt(guarded, resume);
5540        }
5541        self.allow_dynamic_cycle = true;
5542        let point = self.suspend_point(tc, stmt.body());
5543        self.allow_dynamic_cycle = false;
5544        let point = point?;
5545        let guarded = stmt
5546            .body()
5547            .ok_or_else(|| LoweringError::malformed("Timed statement with no guarded body"))?;
5548        if let Some(count) = self.dynamic_cycle_count.take() {
5549            // `##(expr)` with a run-time count (LRM 14.11): `repeat (expr)`
5550            // around one `##1` wait, the count evaluated once on entry. A
5551            // count <= 0 waits for no clock event.
5552            let entry = self.alloc_block();
5553            self.close(cur, Terminator::Fallthrough(entry));
5554            let counter = self.alloc_repeat_counter();
5555            self.set_repeat_init(entry, counter, RepeatCount::Expr(count));
5556            let header = self.alloc_block();
5557            let body = self.alloc_block();
5558            let exit = self.alloc_block();
5559            let resume = self.alloc_block();
5560            self.close(
5561                entry,
5562                Terminator::Loop {
5563                    header,
5564                    body,
5565                    exit,
5566                    entry: header,
5567                },
5568            );
5569            self.close(
5570                header,
5571                Terminator::RepeatCheck {
5572                    counter,
5573                    then_blk: body,
5574                    else_blk: exit,
5575                },
5576            );
5577            self.close(
5578                body,
5579                Terminator::Suspend {
5580                    point,
5581                    resume_blk: resume,
5582                },
5583            );
5584            self.set_repeat_decrement(resume, counter);
5585            self.close(resume, Terminator::Fallthrough(header));
5586            return self.walk_stmt(guarded, exit);
5587        }
5588        let resume = self.alloc_block();
5589        self.close(
5590            cur,
5591            Terminator::Suspend {
5592                point,
5593                resume_blk: resume,
5594            },
5595        );
5596        self.walk_stmt(guarded, resume)
5597    }
5598
5599    /// `lhs = #d rhs;`, `lhs = @(e) rhs;`, `lhs = repeat (n) @(e) rhs;` (LRM
5600    /// 1800-2023 §10.4.1): the right-hand side is evaluated now into a
5601    /// compiler temporary ([`ArgBinding::Capture`]), the process waits, then
5602    /// the temporary is assigned ([`ArgBinding::AssignCaptured`]).
5603    fn walk_intra_assignment_timing(
5604        &mut self,
5605        assign: Expression<'d>,
5606        cur: BlockId,
5607    ) -> Result<BlockId, LoweringError> {
5608        let tc = assign
5609            .assignment_timing()
5610            .and_then(|n| n.as_timing_control())
5611            .ok_or_else(|| LoweringError::malformed("assignment timing is not a timing control"))?;
5612        let target = assign
5613            .left()
5614            .ok_or_else(|| LoweringError::malformed("assignment with no target"))?;
5615        let value = assign
5616            .right()
5617            .ok_or_else(|| LoweringError::malformed("assignment with no value"))?;
5618        let temp = self.captures.len() as u32;
5619        self.captures.push(target);
5620        // A fresh block: its bindings run before anything else in it, and
5621        // the capture must follow `cur`'s own statements.
5622        let capture = self.alloc_block();
5623        self.close(cur, Terminator::Fallthrough(capture));
5624        self.blocks[capture.0 as usize]
5625            .bindings
5626            .push(ArgBinding::Capture { temp, value });
5627        self.drive_clocking = drive_clocking_of(target);
5628        self.drive_via = drive_via_of(target);
5629        let after = if tc.kind() == TimingControlKind::RepeatedEvent {
5630            // `repeat (n) @(e)`: wait for `n` occurrences of `e` -- a counted
5631            // loop around one suspend (a count <= 0 waits for none).
5632            let count = first_expression_child(&tc).ok_or_else(|| {
5633                LoweringError::malformed("repeated event control with no count expression")
5634            })?;
5635            let event = tc
5636                .children()
5637                .into_iter()
5638                .find_map(|n| n.as_timing_control())
5639                .ok_or_else(|| {
5640                    LoweringError::malformed("repeated event control with no event control")
5641                })?;
5642            let point = self.suspend_point(event, None)?;
5643            let counter = self.alloc_repeat_counter();
5644            self.set_repeat_init(capture, counter, RepeatCount::Expr(count));
5645            let header = self.alloc_block();
5646            let body = self.alloc_block();
5647            let exit = self.alloc_block();
5648            let resume = self.alloc_block();
5649            self.close(
5650                capture,
5651                Terminator::Loop {
5652                    header,
5653                    body,
5654                    exit,
5655                    entry: header,
5656                },
5657            );
5658            self.close(
5659                header,
5660                Terminator::RepeatCheck {
5661                    counter,
5662                    then_blk: body,
5663                    else_blk: exit,
5664                },
5665            );
5666            self.close(
5667                body,
5668                Terminator::Suspend {
5669                    point,
5670                    resume_blk: resume,
5671                },
5672            );
5673            self.set_repeat_decrement(resume, counter);
5674            self.close(resume, Terminator::Fallthrough(header));
5675            exit
5676        } else {
5677            let point = self.suspend_point(tc, None)?;
5678            let resume = self.alloc_block();
5679            self.close(
5680                capture,
5681                Terminator::Suspend {
5682                    point,
5683                    resume_blk: resume,
5684                },
5685            );
5686            resume
5687        };
5688        self.drive_clocking = None;
5689        self.drive_via = None;
5690        self.blocks[after.0 as usize]
5691            .bindings
5692            .push(ArgBinding::AssignCaptured {
5693                temp,
5694                target,
5695                nba: false,
5696            });
5697        Ok(after)
5698    }
5699
5700    /// `lhs <= @(e) rhs;`, `lhs <= repeat (n) @(e) rhs;` (LRM 1800-2023
5701    /// §10.4.2, §9.4.5): the right-hand side (and the repeat count) are
5702    /// evaluated now into the compiler temporaries of this process, which
5703    /// goes straight on; a synthetic `join_none` branch, spawned here with
5704    /// copies of those temporaries ([`ProcessCfg::inherited`]), waits for
5705    /// the event control and then stages the nonblocking update.
5706    ///
5707    /// Deviation: the branch is a child of this process like any forked
5708    /// one, so a later `wait fork;` waits for the pending update and
5709    /// `disable fork;` cancels it.
5710    fn walk_nba_event(
5711        &mut self,
5712        stmt: Statement<'d>,
5713        assign: Expression<'d>,
5714        cur: BlockId,
5715    ) -> Result<BlockId, LoweringError> {
5716        let tc = assign
5717            .assignment_timing()
5718            .and_then(|n| n.as_timing_control())
5719            .ok_or_else(|| LoweringError::malformed("assignment timing is not a timing control"))?;
5720        let target = assign
5721            .left()
5722            .ok_or_else(|| LoweringError::malformed("assignment with no target"))?;
5723        let value = assign
5724            .right()
5725            .ok_or_else(|| LoweringError::malformed("assignment with no value"))?;
5726        let temp = self.captures.len() as u32;
5727        self.captures.push(target);
5728        let capture = self.alloc_block();
5729        self.close(cur, Terminator::Fallthrough(capture));
5730        self.blocks[capture.0 as usize]
5731            .bindings
5732            .push(ArgBinding::Capture { temp, value });
5733
5734        // The branch: its own builder, with this walk's context.
5735        let mut b = CfgBuilder::new();
5736        b.this_stack.extend(self.this_stack.last().copied());
5737        b.ref_stack = self.ref_stack.clone();
5738        b.global_clock = self.global_clock;
5739        b.virtual_tasks = self.virtual_tasks.clone();
5740        b.fork_branch_names = self.fork_branch_names.clone();
5741        b.ancestor_blocks = self.ancestor_blocks.clone();
5742        b.default_clocking = self.default_clocking;
5743        b.drive_clocking = drive_clocking_of(target);
5744        b.drive_via = drive_via_of(target);
5745        b.checker_clock = self.checker_clock;
5746        b.captures.push(target);
5747        let mut inherited = vec![InheritedSlot::Temp {
5748            parent: temp,
5749            child: 0,
5750        }];
5751        let entry = b.alloc_block();
5752        let event_roots = vec![target.as_sem_node(), tc.as_sem_node()];
5753        // `lhs <= ##(n) v` with a run-time count (LRM 14.11, 14.16): the count
5754        // is evaluated here and the branch loops one `##1` wait that often,
5755        // like `repeat (n) @(clk)`.
5756        let mut dyn_cycle: Option<(Expression<'d>, SuspendPoint)> = None;
5757        let mut const_point: Option<SuspendPoint> = None;
5758        if tc.kind() == TimingControlKind::CycleDelay {
5759            b.allow_dynamic_cycle = true;
5760            let point = b.suspend_point(tc, None);
5761            b.allow_dynamic_cycle = false;
5762            let point = point?;
5763            match b.dynamic_cycle_count.take() {
5764                Some(count) => dyn_cycle = Some((count, point)),
5765                None => const_point = Some(point),
5766            }
5767        }
5768        let after = if dyn_cycle.is_some() || tc.kind() == TimingControlKind::RepeatedEvent {
5769            let (count, point) = if let Some(d) = dyn_cycle.take() {
5770                d
5771            } else {
5772                let count = first_expression_child(&tc).ok_or_else(|| {
5773                    LoweringError::malformed("repeated event control with no count expression")
5774                })?;
5775                let event = tc
5776                    .children()
5777                    .into_iter()
5778                    .find_map(|n| n.as_timing_control())
5779                    .ok_or_else(|| {
5780                        LoweringError::malformed("repeated event control with no event control")
5781                    })?;
5782                (count, b.suspend_point(event, None)?)
5783            };
5784            // The count is evaluated here, in this process.
5785            let parent_counter = self.alloc_repeat_counter();
5786            self.set_repeat_init(capture, parent_counter, RepeatCount::Expr(count));
5787            let counter = b.alloc_repeat_counter();
5788            inherited.push(InheritedSlot::Counter {
5789                parent: parent_counter,
5790                child: counter,
5791            });
5792            b.set_repeat_init(entry, counter, RepeatCount::Inherited(count));
5793            let header = b.alloc_block();
5794            let body = b.alloc_block();
5795            let exit = b.alloc_block();
5796            let resume = b.alloc_block();
5797            b.close(
5798                entry,
5799                Terminator::Loop {
5800                    header,
5801                    body,
5802                    exit,
5803                    entry: header,
5804                },
5805            );
5806            b.close(
5807                header,
5808                Terminator::RepeatCheck {
5809                    counter,
5810                    then_blk: body,
5811                    else_blk: exit,
5812                },
5813            );
5814            b.close(
5815                body,
5816                Terminator::Suspend {
5817                    point,
5818                    resume_blk: resume,
5819                },
5820            );
5821            b.set_repeat_decrement(resume, counter);
5822            b.close(resume, Terminator::Fallthrough(header));
5823            exit
5824        } else {
5825            let point = match const_point.take() {
5826                Some(p) => p,
5827                None => b.suspend_point(tc, None)?,
5828            };
5829            let resume = b.alloc_block();
5830            b.close(
5831                entry,
5832                Terminator::Suspend {
5833                    point,
5834                    resume_blk: resume,
5835                },
5836            );
5837            resume
5838        };
5839        b.blocks[after.0 as usize]
5840            .bindings
5841            .push(ArgBinding::AssignCaptured {
5842                temp: 0,
5843                target,
5844                nba: true,
5845            });
5846        b.close(after, Terminator::End { loop_back: None });
5847        let inlined_locals = std::mem::take(&mut b.inlined_locals);
5848        let captures = std::mem::take(&mut b.captures);
5849        let ref_bindings = b.take_ref_bindings();
5850        let disable_scopes = b.take_disable_scopes();
5851        let disable_broadcast = std::mem::take(&mut b.disable_broadcast);
5852        let fork_branch_names = std::mem::take(&mut b.fork_branch_names);
5853        let mut branch = ProcessCfg {
5854            blocks: b.finish(),
5855            entry,
5856            body: stmt,
5857            inlined_locals,
5858            captures,
5859            captured: Vec::new(),
5860            ref_bindings,
5861            disable_names: Vec::new(),
5862            fork_branch_names,
5863            disable_scopes,
5864            disable_broadcast,
5865            inherited,
5866            capture_types: Vec::new(),
5867            recursion: Recursion::None,
5868            share_id: None,
5869        };
5870        // It shares the variables its event control and target name.
5871        branch.captured = captured_in(event_roots);
5872        branch
5873            .captured
5874            .retain(|c| !self.ref_stack.iter().any(|(f, _)| f.uid() == c.uid()));
5875        if let Some(t) = self.this_stack.last()
5876            && !branch.captured.iter().any(|c| c.uid() == t.uid())
5877        {
5878            branch.captured.push(*t);
5879        }
5880        let join_blk = self.alloc_block();
5881        self.close(
5882            capture,
5883            Terminator::ForkJoin {
5884                kind: StatementBlockKind::JoinNone,
5885                branches: vec![branch],
5886                join_blk,
5887            },
5888        );
5889        Ok(join_blk)
5890    }
5891
5892    /// A signal event's `(signal, edge)` with a checker's `$inferred_clock` (17.4)
5893    /// replaced by the clocking event of the instantiation context. Any other
5894    /// event is returned as written. A checker instantiated where no clock is
5895    /// inferred is a loud error, never a wait on nothing.
5896    fn resolve_inferred_clock(
5897        &self,
5898        signal: Expression<'d>,
5899        edge: EdgeKind,
5900    ) -> Result<InferredClockEvent<'d>, LoweringError> {
5901        if is_global_clock_call(&signal) {
5902            // `@$global_clock` (14.14, 17.7's counter_model): the global
5903            // clocking's event.
5904            return self.global_clock.ok_or_else(|| {
5905                LoweringError::unsupported(
5906                    "`@$global_clock` as a procedural event control needs a `global clocking` \
5907                     declared in the process's instance or an enclosing one (LRM 14.14); none \
5908                     is in scope",
5909                )
5910            });
5911        }
5912        if !is_inferred_clock_call(&signal) {
5913            return Ok((signal, edge, None));
5914        }
5915        self.checker_clock.ok_or_else(|| {
5916            LoweringError::unsupported(
5917                "`$inferred_clock` (LRM 17.4) as the event control of a checker procedure: the \
5918                 checker is instantiated where no default clocking applies (14.12), so no \
5919                 clock is inferred",
5920            )
5921        })
5922    }
5923
5924    /// The suspend point a timing control waits on; `guarded` is the
5925    /// statement an `@*` implicit event control guards (its reads are the
5926    /// sensitivity).
5927    fn suspend_point(
5928        &mut self,
5929        tc: TimingControl<'d>,
5930        guarded: Option<Statement<'d>>,
5931    ) -> Result<SuspendPoint<'d>, LoweringError> {
5932        let tc_kind = tc.kind();
5933
5934        let point = if tc_kind == TimingControlKind::Invalid {
5935            return Err(LoweringError::malformed("Invalid timing control kind"));
5936        } else if tc_kind == TimingControlKind::Delay {
5937            let expr = first_expression_child(&tc).ok_or_else(|| {
5938                LoweringError::malformed("Delay timing control with no delay expression")
5939            })?;
5940            SuspendPoint::Delay(expr, self.frame_stack.last().map(|f| f.0))
5941        } else if tc_kind == TimingControlKind::SignalEvent {
5942            let edge = tc.edge();
5943            let signal = first_expression_child(&tc).ok_or_else(|| {
5944                LoweringError::malformed("SignalEvent timing control with no signal expression")
5945            })?;
5946            reject_chandle_event(&signal)?;
5947            let iff = extract_iff(&tc);
5948            let (signal, edge, inferred_iff) = self.resolve_inferred_clock(signal, edge)?;
5949            // `@clock iff c` on an inferred clock that itself has an `iff`
5950            // needs both guards (a conjunction, LRM 17.4): a one-member
5951            // event list carries them; `Edge` has room for one only.
5952            if let (Some(a), Some(b)) = (iff, inferred_iff) {
5953                if edge == EdgeKind::PosEdge || edge == EdgeKind::NegEdge {
5954                    self.note_edge_signal(signal, edge);
5955                }
5956                return Ok(SuspendPoint::EdgeList(
5957                    vec![(signal, edge)],
5958                    vec![vec![a, b]],
5959                ));
5960            }
5961            let iff = iff.or(inferred_iff);
5962            if edge != EdgeKind::PosEdge && edge != EdgeKind::NegEdge {
5963                // `@(a)` (any change) / `@(edge a)`: a one-entry event list,
5964                // resolved through the program's wait table like `@(a or b)`.
5965                // With `iff` (LRM 1800-2023 9.4.2.3: `@(e iff c)` waits for a
5966                // change of `e`, or for the event, and then for `c`) it is a
5967                // single `Edge` term carrying its guard.
5968                if iff.is_some() {
5969                    SuspendPoint::Edge { signal, edge, iff }
5970                } else {
5971                    SuspendPoint::EdgeList(vec![(signal, edge)], vec![Vec::new()])
5972                }
5973            } else {
5974                self.note_edge_signal(signal, edge);
5975                SuspendPoint::Edge { signal, edge, iff }
5976            }
5977        } else if tc_kind == TimingControlKind::EventList {
5978            // Task 1: `@(a or b or c)` -- wake on any member. Each child is
5979            // itself a nested `TimingControl` (in practice always
5980            // `SignalEvent`, per the grammar). A member's `iff` (LRM
5981            // 1800-2023 9.4.2.3) rides along per member in the point's
5982            // second field.
5983            let mut entries = Vec::new();
5984            let mut iffs = Vec::new();
5985            for child in tc.children() {
5986                let sub_tc = child.as_timing_control().ok_or_else(|| {
5987                    LoweringError::malformed(
5988                        "EventList timing control with a non-timing-control child",
5989                    )
5990                })?;
5991                let edge = sub_tc.edge();
5992                let signal = first_expression_child(&sub_tc).ok_or_else(|| {
5993                    LoweringError::malformed(
5994                        "EventList member timing control with no signal expression",
5995                    )
5996                })?;
5997                if self.skip_class_event_terms && is_class_event_expr(&signal) {
5998                    continue;
5999                }
6000                reject_chandle_event(&signal)?;
6001                let iff = extract_iff(&sub_tc);
6002                let (signal, edge, inferred_iff) = self.resolve_inferred_clock(signal, edge)?;
6003                if edge == EdgeKind::PosEdge || edge == EdgeKind::NegEdge {
6004                    self.note_edge_signal(signal, edge);
6005                }
6006                entries.push((signal, edge));
6007                iffs.push(iff.into_iter().chain(inferred_iff).collect());
6008            }
6009            if entries.is_empty() {
6010                return Err(LoweringError::malformed(
6011                    "EventList timing control with no member event controls",
6012                ));
6013            }
6014            SuspendPoint::EdgeList(entries, iffs)
6015        } else if tc_kind == TimingControlKind::CycleDelay {
6016            // Task 1 shape, Task 6 (CG-171) revised semantics -- see
6017            // `SuspendPoint::CycleDelay`'s own doc comment for the full
6018            // reasoning. Still a named `LoweringError`, never a silent
6019            // "assume 1 clock unit", when there is no unambiguous single
6020            // clock in scope.
6021            let count_expr = first_expression_child(&tc).ok_or_else(|| {
6022                LoweringError::malformed("CycleDelay timing control with no count expression")
6023            })?;
6024            let constant = count_expr
6025                .constant_value()
6026                .and_then(|v| v.as_i64())
6027                .and_then(|v| u32::try_from(v).ok());
6028            let count = match constant {
6029                Some(c) => c,
6030                None if self.allow_dynamic_cycle => {
6031                    self.dynamic_cycle_count = Some(count_expr);
6032                    1
6033                }
6034                None => {
6035                    return Err(LoweringError::unsupported(
6036                        "a non-constant cycle delay `##(expr)` in an intra-assignment timing \
6037                         control (LRM 14.11 allows a run-time count only as a statement \
6038                         delay; this simulator needs a compile-time constant here)",
6039                    ));
6040                }
6041            };
6042            // The default clocking block's event (LRM 1800-2023 §14.11),
6043            // else the process's own inferred clock.
6044            let (clock, edge) = match (
6045                self.drive_clocking.or(self.default_clocking),
6046                self.inferred_clock,
6047            ) {
6048                (Some(cb), _) => (cb, EdgeKind::BothEdges),
6049                (None, Some(Some((signal, edge)))) => (
6050                    signal.referenced_symbol().ok_or_else(|| {
6051                        LoweringError::unsupported("CycleDelay (`##N`) clock with no symbol")
6052                    })?,
6053                    edge,
6054                ),
6055                _ => {
6056                    return Err(LoweringError::unsupported(
6057                        "CycleDelay (`##N`) with no unambiguous single inferred clock in scope \
6058                         (no enclosing posedge/negedge edge, or more than one distinct clock \
6059                         signal/edge seen) -- ARCHITECTURE §6.5's own \"ClockDomain the scheduler \
6060                         needs\" requirement",
6061                    ));
6062                }
6063            };
6064            let via = if self.drive_clocking.is_some() {
6065                self.drive_via
6066            } else {
6067                None
6068            };
6069            SuspendPoint::CycleDelay {
6070                count,
6071                clock,
6072                edge,
6073                via,
6074            }
6075        } else if tc_kind == TimingControlKind::ImplicitEvent {
6076            // Task 6 (CG-081): `@*`/`@(*)`. See `SuspendPoint::ImplicitComb`'s
6077            // own doc comment for why the read-set is captured here (AST-only,
6078            // no live Analysis handle reaches this crate).
6079            let body_stmt = guarded.ok_or_else(|| {
6080                LoweringError::unsupported(
6081                    "an `@*` event control with no guarded statement (an intra-assignment `@*`)",
6082                )
6083            })?;
6084            SuspendPoint::ImplicitComb(implicit_comb_read_set(&body_stmt))
6085        } else if tc_kind == TimingControlKind::RepeatedEvent {
6086            // Task 6 (CG-081): `repeat(count) @(...)`. **Verified,
6087            // structurally unreachable at this dispatch point, never a scope
6088            // choice** -- empirically probed against the pinned
6089            // `sv-lang`=0.1.9 parser (a throwaway test compiling `initial q
6090            // <= repeat(3) @(posedge clk) 1;` and dumping the AST): a
6091            // `RepeatedEventControl` node is NEVER the outer `TimingControl`
6092            // of a `StatementKind::Timed` node at all. Two independent
6093            // parser facts combine to make this so: (1) `Parser::
6094            // parseStatement`'s own dispatch switch (`Parser_statements.cpp`)
6095            // sends a leading `repeat` keyword straight to
6096            // `parseLoopStatement` (producing `StatementKind::RepeatLoop`,
6097            // already handled by `walk_stmt`/`is_ordinary_statement_kind`) --
6098            // it never reaches `parseTimingControl()` at all, so
6099            // `repeat(3) @(posedge clk) q = 1;` used directly as a statement
6100            // parses as "run `@(posedge clk) q = 1;` 3 times", not "wait for
6101            // clk's 3rd posedge, then run `q = 1;` once". (2) The ONLY
6102            // grammar position `parseTimingControl()`'s own `RepeatKeyword`
6103            // case (`Parser_expressions.cpp`) is ever reached from is
6104            // `parseSubExpression`'s `isPossibleDelayOrEventControl` check --
6105            // i.e. `RepeatedEventControl` is only ever constructible as an
6106            // INTRA-ASSIGNMENT timing control embedded in an `Assignment`
6107            // expression's own `.children()` (confirmed empirically: `q <=
6108            // repeat(3) @(posedge clk) 1;` parses as one
6109            // `StatementKind::ExpressionStatement` whose `Assignment`
6110            // expression's `children()` is `[TimingControl(RepeatedEvent),
6111            // NamedValue, Conversion]` -- verified against
6112            // `AssignmentExpression::visitExprs` in the vendored slang C++,
6113            // `if (timingControl) timingControl->visit(visitor); left()...;
6114            // right()...;`), never a `Statement::timing_control()` at all.
6115            // The intra-assignment form is lowered by
6116            // `walk_intra_assignment_timing` (the right-hand side captured in
6117            // a `Temporary` frame slot before the counted wait), which asks
6118            // this function only for the inner event control's suspend
6119            // point -- so reaching this arm means a malformed AST.
6120            return Err(LoweringError::unsupported(
6121                "RepeatedEvent timing control (`repeat(n) @(...)`) can never be the outer \
6122                 TimingControlKind of a Timed statement -- `repeat` at statement-position \
6123                 always parses as StatementKind::RepeatLoop instead, and RepeatedEventControl \
6124                 is only ever producible as an intra-assignment timing control, which \
6125                 walk_intra_assignment_timing lowers",
6126            ));
6127        } else if tc_kind == TimingControlKind::OneStepDelay {
6128            // Task 6 (CG-081): `#0` / the built-in `1step` delay -- see
6129            // `SuspendPoint::OneStep`'s own doc comment.
6130            SuspendPoint::OneStep
6131        } else if tc_kind == TimingControlKind::BlockEventList {
6132            // LRM 19.3: a block event list `@@(begin f)` is only ever a
6133            // covergroup's sampling event (lowered by `covergroup` block-event
6134            // registration); slang rejects it as a procedural timing control
6135            // at parse ("expected statement"), so this arm is unreachable.
6136            return Err(LoweringError::unsupported(
6137                "a block event list `@@(...)` as a procedural timing control (LRM 19.3: \
6138                 only a covergroup sampling event)",
6139            ));
6140        } else if tc_kind == TimingControlKind::Delay3 {
6141            // Delay3Control (`#(d1,d2,d3)`) is gate-primitive rise/fall/
6142            // turn-off delay syntax (LRM 28.14) -- not applicable to a
6143            // procedural `Timed` statement's suspend point at all (CG-081's
6144            // own 8-kind enumeration excludes it), so this stays a named
6145            // `LoweringError` unconditionally, not a gap this task closes.
6146            return Err(LoweringError::unsupported(
6147                "Delay3 timing control (gate-primitive rise/fall/turn-off delay syntax -- not \
6148                 applicable to a procedural statement's suspend point)",
6149            ));
6150        } else {
6151            // Unreachable as long as `determinism::kind_guard`'s pinned
6152            // `TimingControlKind::COUNT == 10` still holds -- see
6153            // `walk_stmt`'s identical reasoning for `StatementKind`.
6154            return Err(LoweringError::malformed(format!(
6155                "unrecognized TimingControlKind {tc_kind:?} (sv-lang-kinds version drift -- \
6156                 see determinism::kind_guard's TimingControlKind pin)"
6157            )));
6158        };
6159        Ok(point)
6160    }
6161
6162    /// Task 3 (PR-006's shape from Task 1, real semantics from CG-169/
6163    /// CG-115 here): `wait(expr) stmt;` (LRM 9.4.3). Unlike every other
6164    /// `Timed`/`Wait`-family suspend point, this one does NOT
6165    /// unconditionally close `cur` with `Terminator::Suspend` -- LRM 9.4.3 is
6166    /// an *immediate check with a conditional suspend*: if `expr` is already
6167    /// true the moment execution reaches it, there is NO suspend/resume
6168    /// round-trip at all (straight fallthrough into the guarded body, same
6169    /// call); only a false evaluation actually suspends. This is
6170    /// structurally the same "sometimes there's no suspend point at all"
6171    /// shape `docs/design/process.md` §2 calls out by name, and it is
6172    /// represented here exactly like any other runtime-decided branch: a
6173    /// `Terminator::Branch` whose `then_blk` is the guarded body's own entry
6174    /// block (`resume`) and whose `else_blk` is a fresh block that alone
6175    /// carries the real `Terminator::Suspend{point: SuspendPoint::Level(cond),
6176    /// resume_blk: resume}` -- `resume` is deliberately the SAME `BlockId` on
6177    /// both edges, so there genuinely is a suspend-free path from `cur` to
6178    /// the guarded body in the built `ProcessCfg` (a purely structural,
6179    /// compile-time fact -- codegen decides which edge is taken at
6180    /// simulation runtime by reusing the exact same generic branch-condition
6181    /// lowering `if`/`while` already use, `docs/design/process.md`'s own
6182    /// `is_definitely_true` reduction list explicitly includes "wait()
6183    /// condition" alongside "if"/"while" for this reason, PR-069's
6184    /// `Terminator`/`SuspendPoint` "cross-check against
6185    /// contains_suspend_trigger" extension policy). `resume`'s own entry is
6186    /// therefore executed either way (already-true fallthrough, or a genuine
6187    /// resume from the scheduler after the `Suspend` fires) -- the
6188    /// resume-prologue re-check that makes a spurious wake re-suspend
6189    /// without running the body is CG-115's own job, entirely on the
6190    /// codegen side (`vj-codegen::process_lower::compile_iff_guard`);
6191    /// this crate never evaluates `expr` itself (no runtime state reaches
6192    /// this AST-only builder).
6193    fn walk_wait(&mut self, stmt: Statement<'d>, cur: BlockId) -> Result<BlockId, LoweringError> {
6194        let cond = stmt.condition().ok_or_else(|| {
6195            LoweringError::malformed("Wait statement with no condition expression")
6196        })?;
6197        let resume = self.alloc_block();
6198        let suspend_blk = self.alloc_block();
6199        self.close(
6200            cur,
6201            Terminator::Branch {
6202                cond,
6203                then_blk: resume,
6204                else_blk: suspend_blk,
6205            },
6206        );
6207        self.close(
6208            suspend_blk,
6209            Terminator::Suspend {
6210                point: SuspendPoint::Level(cond),
6211                resume_blk: resume,
6212            },
6213        );
6214        let guarded = stmt
6215            .body()
6216            .ok_or_else(|| LoweringError::malformed("Wait statement with no guarded body"))?;
6217        self.walk_stmt(guarded, resume)
6218    }
6219
6220    /// `-> ev;` / `->> ev;` / `->> #d ev;` / `->> @(ctl) ev;` (LRM 1800-2023
6221    /// 15.5.1, 15.5.2). An `EventTrigger` with no timing, or with a `#d`
6222    /// delay, never suspends the issuer -- it is one ordinary leaf (codegen
6223    /// stages the delayed trigger as a nonblocking update). One with an event
6224    /// control (`->> @(ctl) ev;`) forks a detached one-shot process that
6225    /// waits on `ctl` and then triggers, so the issuer continues at once
6226    /// (`spawn_detached_trigger`).
6227    fn walk_event_trigger(
6228        &mut self,
6229        stmt: Statement<'d>,
6230        cur: BlockId,
6231    ) -> Result<BlockId, LoweringError> {
6232        let detached = std::mem::take(&mut self.detached_trigger);
6233        let Some(timing_node) = stmt.timing() else {
6234            self.push(cur, stmt);
6235            return Ok(cur);
6236        };
6237        let tc = timing_node.as_timing_control().ok_or_else(|| {
6238            LoweringError::malformed("EventTrigger timing is not a TimingControl node")
6239        })?;
6240        if tc.kind() == TimingControlKind::Delay {
6241            // `->> #d e;` never blocks the issuer: the trigger is staged as a
6242            // nonblocking update `d` ticks out (codegen reads the delay).
6243            self.push(cur, stmt);
6244            return Ok(cur);
6245        }
6246        if !detached {
6247            // `->> @(ctl) e;`: the issuer continues at once; a detached
6248            // process waits for `ctl`, then stages the trigger.
6249            return self.spawn_detached_trigger(stmt, cur, ProcessLoopKind::OneShot);
6250        }
6251        let point = self.suspend_point(tc, None)?;
6252        let resume = self.alloc_block();
6253        self.close(
6254            cur,
6255            Terminator::Suspend {
6256                point,
6257                resume_blk: resume,
6258            },
6259        );
6260        self.push(resume, stmt);
6261        Ok(resume)
6262    }
6263
6264    /// Forks the detached one-shot process of `->> @(ctl) e;` (see
6265    /// [`Self::detached_trigger`]) and continues the issuer past it.
6266    fn spawn_detached_trigger(
6267        &mut self,
6268        stmt: Statement<'d>,
6269        cur: BlockId,
6270        loop_kind: ProcessLoopKind,
6271    ) -> Result<BlockId, LoweringError> {
6272        let mut branch = build_cfg_in(
6273            &stmt,
6274            loop_kind,
6275            None,
6276            self.default_clocking,
6277            None,
6278            self.checker_clock,
6279            Inherited {
6280                this_var: self.this_stack.last().copied(),
6281                ref_aliases: self.ref_stack.clone(),
6282                global_clock: self.global_clock,
6283                detached_trigger: true,
6284                forced_extern: None,
6285                rec_outer: self.rec_outer.clone(),
6286                fork_memo: Some(self.fork_memo.clone()),
6287                rec: None,
6288                rec_up: Vec::new(),
6289                known_fork_labels: Vec::new(),
6290                ancestor_blocks: Vec::new(),
6291            },
6292            self.virtual_tasks.clone(),
6293        )?;
6294        branch.captured = captured_vars(&branch);
6295        branch
6296            .captured
6297            .retain(|c| !self.ref_stack.iter().any(|(f, _)| f.uid() == c.uid()));
6298        if let Some(t) = self.this_stack.last()
6299            && !branch.captured.iter().any(|c| c.uid() == t.uid())
6300        {
6301            branch.captured.push(*t);
6302        }
6303        let join_blk = self.alloc_block();
6304        self.close(
6305            cur,
6306            Terminator::ForkJoin {
6307                kind: StatementBlockKind::JoinNone,
6308                branches: vec![branch],
6309                join_blk,
6310            },
6311        );
6312        Ok(join_blk)
6313    }
6314
6315    /// `$async$and$array(mem, in, out);` and its seven siblings (LRM 1800-2023
6316    /// 20.16.1): the logic array is evaluated at the call, and again whenever
6317    /// an input term or any word of the personality memory changes. The
6318    /// issuer evaluates once (the call is an ordinary leaf) and forks a
6319    /// detached looping process that evaluates, waits on the operands' read
6320    /// set, and re-runs the same statement on each wake (the
6321    /// `detached_trigger` flag marks that process's own copy, so it does not
6322    /// fork again).
6323    fn walk_async_pla(
6324        &mut self,
6325        stmt: Statement<'d>,
6326        cur: BlockId,
6327    ) -> Result<BlockId, LoweringError> {
6328        let detached = std::mem::take(&mut self.detached_trigger);
6329        if !detached {
6330            self.push(cur, stmt);
6331            return self.spawn_detached_trigger(stmt, cur, ProcessLoopKind::Looping);
6332        }
6333        // Evaluate first, then wait: an input that changed between the fork
6334        // and this process's first run (same time step) is not missed.
6335        let reads = async_pla_read_set(&stmt);
6336        let resume = self.alloc_block();
6337        self.push(cur, stmt);
6338        self.close(
6339            cur,
6340            Terminator::Suspend {
6341                point: SuspendPoint::ImplicitComb(reads),
6342                resume_blk: resume,
6343            },
6344        );
6345        Ok(resume)
6346    }
6347
6348    /// A call statement `stmt` to the interface `extern` method `targets`
6349    /// (LRM 25.7), when the call needs the CFG: it runs the implementation
6350    /// inline (one), or as the branches of a `fork`...`join` (an `extern
6351    /// forkjoin` task with several, 25.7.5), or reports the run-time error of
6352    /// a forkjoin task nothing implements and returns with no effect.
6353    /// `None` when the call is an ordinary leaf (every implementation runs
6354    /// without suspending: the code generator calls them in turn).
6355    fn walk_extern_call(
6356        &mut self,
6357        stmt: Statement<'d>,
6358        call: Expression<'d>,
6359        targets: &crate::netlist::itf::ExternTargets<'d>,
6360        cur: BlockId,
6361    ) -> Result<Option<BlockId>, LoweringError> {
6362        // One branch of an enclosing fork: this implementation only.
6363        if let Some(imp) = self.forced_extern.take() {
6364            return self.inline_task_call(call, imp, cur).map(Some);
6365        }
6366        if targets.impls.is_empty() {
6367            let join = self.alloc_block();
6368            self.close(
6369                cur,
6370                Terminator::Switch {
6371                    on: SwitchOn::NoTarget {
6372                        what: format!(
6373                            "no module connected to the interface implements extern forkjoin \
6374                             task `{}`; the call has no effect",
6375                            targets.proto.name()
6376                        ),
6377                    },
6378                    arms: Vec::new(),
6379                    default_blk: join,
6380                    report: Some(call.source_range().start),
6381                },
6382            );
6383            return Ok(Some(join));
6384        }
6385        if !targets.impls.iter().any(subroutine_can_suspend) {
6386            return Ok(None);
6387        }
6388        if targets.impls.len() == 1 {
6389            return self.inline_task_call(call, targets.impls[0], cur).map(Some);
6390        }
6391        // Every implementation as a branch of one `fork`...`join` (the call
6392        // returns when all have).
6393        let mut branches = Vec::with_capacity(targets.impls.len());
6394        for imp in &targets.impls {
6395            let mut branch = build_cfg_in(
6396                &stmt,
6397                ProcessLoopKind::OneShot,
6398                None,
6399                self.default_clocking,
6400                None,
6401                self.checker_clock,
6402                Inherited {
6403                    this_var: self.this_stack.last().copied(),
6404                    ref_aliases: self.ref_stack.clone(),
6405                    global_clock: self.global_clock,
6406                    detached_trigger: false,
6407                    forced_extern: Some(*imp),
6408                    rec_outer: self.rec_outer.clone(),
6409                    fork_memo: Some(self.fork_memo.clone()),
6410                    rec: None,
6411                    rec_up: Vec::new(),
6412                    known_fork_labels: self.fork_branch_names.clone(),
6413                    ancestor_blocks: self.ancestor_blocks.clone(),
6414                },
6415                self.virtual_tasks.clone(),
6416            )?;
6417            branch.captured = captured_vars(&branch);
6418            branch
6419                .captured
6420                .retain(|c| !self.ref_stack.iter().any(|(f, _)| f.uid() == c.uid()));
6421            if let Some(t) = self.this_stack.last()
6422                && !branch.captured.iter().any(|c| c.uid() == t.uid())
6423            {
6424                branch.captured.push(*t);
6425            }
6426            branches.push(branch);
6427        }
6428        let join_blk = self.alloc_block();
6429        self.close(
6430            cur,
6431            Terminator::ForkJoin {
6432                kind: StatementBlockKind::JoinAll,
6433                branches,
6434                join_blk,
6435            },
6436        );
6437        Ok(Some(join_blk))
6438    }
6439
6440    /// PR-045 (Task 1): `while (cond) body;`. `header` evaluates `cond`
6441    /// every iteration (re-checked, not cached) and branches to `body` or
6442    /// `exit`; `body`'s own exit closes the back edge to `header`; the real
6443    /// entry from `cur` is `header` (the condition is checked before the
6444    /// first iteration).
6445    fn walk_while_loop(
6446        &mut self,
6447        stmt: Statement<'d>,
6448        cur: BlockId,
6449    ) -> Result<BlockId, LoweringError> {
6450        let cond = stmt
6451            .condition()
6452            .ok_or_else(|| LoweringError::malformed("WhileLoop with no condition expression"))?;
6453        let body_stmt = stmt
6454            .body()
6455            .ok_or_else(|| LoweringError::malformed("WhileLoop with no body statement"))?;
6456
6457        let header = self.alloc_block();
6458        let body = self.alloc_block();
6459        let exit = self.alloc_block();
6460        // 38.36.1.2: before the condition, on every iteration.
6461        self.probe(header, stmt);
6462        self.close(
6463            cur,
6464            Terminator::Loop {
6465                header,
6466                body,
6467                exit,
6468                entry: header,
6469            },
6470        );
6471        self.close(
6472            header,
6473            Terminator::Branch {
6474                cond,
6475                then_blk: body,
6476                else_blk: exit,
6477            },
6478        );
6479        let body_exit = self.walk_loop_body(body_stmt, body, header, exit)?;
6480        self.close(body_exit, Terminator::Fallthrough(header));
6481        Ok(exit)
6482    }
6483
6484    /// PR-045 (Task 1): `do body; while (cond);`. Unlike `while`, `body`
6485    /// always runs once *before* any check -- the real entry from `cur` is
6486    /// `body` itself (`Terminator::Loop::entry`), and `header` (the
6487    /// post-body recheck, reached only via `body`'s own back edge) branches
6488    /// back to `body` or out to `exit`.
6489    fn walk_do_while_loop(
6490        &mut self,
6491        stmt: Statement<'d>,
6492        cur: BlockId,
6493    ) -> Result<BlockId, LoweringError> {
6494        let cond = stmt
6495            .condition()
6496            .ok_or_else(|| LoweringError::malformed("DoWhileLoop with no condition expression"))?;
6497        let body_stmt = stmt
6498            .body()
6499            .ok_or_else(|| LoweringError::malformed("DoWhileLoop with no body statement"))?;
6500
6501        let header = self.alloc_block();
6502        let body = self.alloc_block();
6503        let exit = self.alloc_block();
6504        self.close(
6505            cur,
6506            Terminator::Loop {
6507                header,
6508                body,
6509                exit,
6510                entry: body,
6511            },
6512        );
6513        self.close(
6514            header,
6515            Terminator::Branch {
6516                cond,
6517                then_blk: body,
6518                else_blk: exit,
6519            },
6520        );
6521        let body_exit = self.walk_loop_body(body_stmt, body, header, exit)?;
6522        self.close(body_exit, Terminator::Fallthrough(header));
6523        Ok(exit)
6524    }
6525
6526    /// PR-045 (Task 1): `forever body;`. No condition exists at all (LRM:
6527    /// unconditional) -- `header` unconditionally falls through to `body`,
6528    /// never to `exit`, so `exit` is allocated (every `Terminator::Loop`
6529    /// always names one, per PR-045's literal shape) but unreachable by any
6530    /// statically-analyzable path; only `disable`/`$finish` end a `forever`
6531    /// loop in practice, neither of which this function needs to wire.
6532    fn walk_forever_loop(
6533        &mut self,
6534        stmt: Statement<'d>,
6535        cur: BlockId,
6536    ) -> Result<BlockId, LoweringError> {
6537        let body_stmt = stmt
6538            .body()
6539            .ok_or_else(|| LoweringError::malformed("ForeverLoop with no body statement"))?;
6540
6541        let header = self.alloc_block();
6542        let body = self.alloc_block();
6543        let exit = self.alloc_block();
6544        // 38.36.1.2: when first encountered and on every iteration.
6545        self.probe(header, stmt);
6546        self.close(
6547            cur,
6548            Terminator::Loop {
6549                header,
6550                body,
6551                exit,
6552                entry: header,
6553            },
6554        );
6555        self.close(header, Terminator::Fallthrough(body));
6556        let body_exit = self.walk_loop_body(body_stmt, body, header, exit)?;
6557        self.close(body_exit, Terminator::Fallthrough(header));
6558        Ok(exit)
6559    }
6560
6561    /// PR-045 (Task 1): `for (init; cond; step) body;`. The declaration-form
6562    /// initializer (`for (int i = 0; ...)`) is a separate `VariableDeclaration`
6563    /// statement slang hoists *before* this `ForLoop` node in an implicit
6564    /// wrapper `Block` -- already handled for free by the ordinary `List`/
6565    /// `Block` walk, so this function never sees it. The pre-declared-var
6566    /// form (`for (i = 0; ...)`, `for_loop_initializers()` non-empty) is a
6567    /// named `LoweringError`: those initializers are bare `Expression`s, and
6568    /// (like `for_loop_steps()`, see `BasicBlock::step_exprs`'s doc comment)
6569    /// this crate has no way to run a bare pre-loop `Expression` as an
6570    /// ordinary leaf effect without fabricating a `Statement` node it does
6571    /// not have -- a real, if narrow, gap recorded in this task's
6572    /// deviations, not a silent drop. The condition (`stop expression`) is
6573    /// re-checked every iteration in `header`; the step expressions run via
6574    /// `step_exprs` on a dedicated `step` block between `body` and the back
6575    /// edge to `header`.
6576    fn walk_for_loop(
6577        &mut self,
6578        stmt: Statement<'d>,
6579        cur: BlockId,
6580    ) -> Result<BlockId, LoweringError> {
6581        // The classic Verilog form `for (i = 0; ...)` (pre-declared `i`):
6582        // its bare initializer expressions run as `step_exprs` on `cur`
6583        // itself -- after `cur`'s own statements, before the jump into
6584        // `header` -- the same bare-expression mechanism the loop's step
6585        // expressions use below.
6586        // 38.36.1.2: before any of the control expressions (the initializers
6587        // run as `cur`'s step expressions, after its statements) ...
6588        self.probe(cur, stmt);
6589        for init in stmt.for_loop_initializers() {
6590            self.push_step_expr(cur, init);
6591        }
6592        // `for (;;)`: an omitted stop expression is always true (LRM
6593        // 1800-2023 12.7.1), so `header` falls through to `body` unconditionally.
6594        let cond = stmt.condition();
6595        let body_stmt = stmt
6596            .body()
6597            .ok_or_else(|| LoweringError::malformed("ForLoop with no body statement"))?;
6598        let steps = stmt.for_loop_steps();
6599
6600        let header = self.alloc_block();
6601        let body = self.alloc_block();
6602        let exit = self.alloc_block();
6603        self.close(
6604            cur,
6605            Terminator::Loop {
6606                header,
6607                body,
6608                exit,
6609                entry: header,
6610            },
6611        );
6612        match cond {
6613            Some(cond) => self.close(
6614                header,
6615                Terminator::Branch {
6616                    cond,
6617                    then_blk: body,
6618                    else_blk: exit,
6619                },
6620            ),
6621            None => self.close(header, Terminator::Fallthrough(body)),
6622        }
6623        let step = self.alloc_block();
6624        let body_exit = self.walk_loop_body(body_stmt, body, step, exit)?;
6625        self.close(body_exit, Terminator::Fallthrough(step));
6626        // ... and on every iteration before the incremental statement.
6627        self.probe(step, stmt);
6628        for s in steps {
6629            self.push_step_expr(step, s);
6630        }
6631        self.close(step, Terminator::Fallthrough(header));
6632        Ok(exit)
6633    }
6634
6635    /// PR-045: `foreach (arr[i, j, ...]) body;`. A dimension's bound is a
6636    /// static `ConstantRange` (`Statement::foreach_loop_dims`), never an
6637    /// `Expression`, so each named dimension is a counted loop on the
6638    /// `repeat` machinery (`RepeatCount::Const` elements, a `RepeatCheck`
6639    /// header), its index variable set by `ArgBinding::Const`/`Step`. The
6640    /// body is lowered once. A dynamically-sized dimension (`range() ==
6641    /// None`) is a named `LoweringError`.
6642    fn walk_foreach_loop(
6643        &mut self,
6644        stmt: Statement<'d>,
6645        cur: BlockId,
6646    ) -> Result<BlockId, LoweringError> {
6647        // Over a queue, dynamic or associative array: a leaf statement
6648        // codegen lowers as one loop (`stmt_shared::lower_collection_
6649        // foreach`) when its body cannot suspend.
6650        let over_collection = stmt
6651            .expressions()
6652            .first()
6653            .and_then(|a| a.expr_type())
6654            .is_some_and(|t| crate::state::storage::is_collection(&t));
6655        // A suspending body over a collection nested in a collection or in a
6656        // fixed-size array (`int a[2][]`, `int q[$][$]`): `walk_nested_foreach`.
6657        let nested_dyn = stmt
6658            .foreach_loop_dims()
6659            .iter()
6660            .skip(usize::from(over_collection))
6661            .any(|d| d.range().is_none());
6662        // A `return` in the body of an inlined task has to jump out of the
6663        // CFG's task body (LRM 1800-2023 13.4.1), which a leaf loop cannot do.
6664        let returns = escapes(stmt.as_sem_node(), false, !self.return_targets.is_empty());
6665        if nested_dyn && (contains_suspend_trigger(stmt.as_sem_node()) || returns) {
6666            return self.walk_nested_foreach(stmt, cur);
6667        }
6668        if over_collection {
6669            if contains_suspend_trigger(stmt.as_sem_node()) || returns {
6670                return self.walk_collection_foreach(stmt, cur);
6671            }
6672            self.push(cur, stmt);
6673            return Ok(cur);
6674        }
6675        // A fixed-size array of queues / dynamic arrays (`int a[3][]`): one
6676        // leaf loop when the body cannot suspend (`stmt_shared::
6677        // lower_fixed_outer_foreach`).
6678        if stmt.foreach_loop_dims().iter().any(|d| d.range().is_none())
6679            && !contains_suspend_trigger(stmt.as_sem_node())
6680            && !returns
6681        {
6682            self.push(cur, stmt);
6683            return Ok(cur);
6684        }
6685        let dims = stmt.foreach_loop_dims();
6686        let body_stmt = stmt
6687            .body()
6688            .ok_or_else(|| LoweringError::malformed("ForeachLoop with no body statement"))?;
6689        // One counted loop per named dimension, outermost first; a dimension
6690        // whose loop variable is omitted (`foreach (a[i, ])`) is not
6691        // iterated (LRM 1800-2023 §12.7.3).
6692        let mut loops = Vec::new();
6693        for d in &dims {
6694            let Some(var) = d.loop_var() else {
6695                continue;
6696            };
6697            let range = d.range().ok_or_else(|| {
6698                LoweringError::unsupported(
6699                    "foreach over a dynamically-sized dimension (a dynamic array, queue or \
6700                     associative array)",
6701                )
6702            })?;
6703            loops.push((var, i64::from(range.left), i64::from(range.right)));
6704        }
6705        // Every index omitted: no dimension is iterated, the body never runs.
6706        if loops.is_empty() {
6707            return Ok(cur);
6708        }
6709        self.walk_foreach_nest(&loops, body_stmt, cur, None)
6710    }
6711
6712    /// A suspending `foreach` where a queue / dynamic array is nested in a
6713    /// fixed-size array or in another collection (`int a[2][3][]`, `int
6714    /// d[][]`, `int q[2][$]`). One counted loop per named dimension,
6715    /// outermost first (LRM 1800-2023 12.7.3); a fixed dimension counts its
6716    /// static range, a collection dimension counts the size of the element
6717    /// reached through the enclosing indices ([`RepeatCount::NestedSize`]),
6718    /// read when that loop is entered. An omitted index in front of a
6719    /// collection dimension and an associative array in the chain are named
6720    /// errors (the leaf lowering refuses the same shapes).
6721    fn walk_nested_foreach(
6722        &mut self,
6723        stmt: Statement<'d>,
6724        cur: BlockId,
6725    ) -> Result<BlockId, LoweringError> {
6726        let arr = stmt.expressions()[0];
6727        let body_stmt = stmt
6728            .body()
6729            .ok_or_else(|| LoweringError::malformed("ForeachLoop with no body statement"))?;
6730        let dims = stmt.foreach_loop_dims();
6731        let last_dyn = dims.iter().rposition(|d| d.range().is_none());
6732        // Whether each dimension is an associative array (its variable is a
6733        // key): peel the array type one dimension at a time.
6734        let mut ty = arr.expr_type();
6735        let mut assoc_dims = Vec::new();
6736        for _ in &dims {
6737            assoc_dims.push(ty.as_ref().is_some_and(|t| t.is_associative_array()));
6738            ty = ty.and_then(|t| t.element_type());
6739        }
6740        let mut levels = Vec::new();
6741        for (k, d) in dims.iter().enumerate() {
6742            let Some(var) = d.loop_var() else {
6743                if last_dyn.is_some_and(|l| l > k) {
6744                    return Err(LoweringError::unsupported(
6745                        "foreach whose body waits with an omitted index in front of a nested \
6746                         queue, dynamic-array or associative-array dimension",
6747                    ));
6748                }
6749                continue;
6750            };
6751            let fixed = d.range().map(|r| (r.left, r.right));
6752            levels.push(NestStep {
6753                var,
6754                fixed,
6755                assoc: assoc_dims[k],
6756            });
6757        }
6758        self.walk_nest_levels(&levels, &mut Vec::new(), arr, body_stmt, cur, None)
6759    }
6760
6761    /// The loops of [`Self::walk_nested_foreach`], `levels[0]` outermost;
6762    /// `prefix` holds the enclosing levels. `break` leaves the whole nest.
6763    fn walk_nest_levels(
6764        &mut self,
6765        levels: &[NestStep<'d>],
6766        prefix: &mut Vec<NestStep<'d>>,
6767        arr: Expression<'d>,
6768        body_stmt: Statement<'d>,
6769        cur: BlockId,
6770        outer_exit: Option<BlockId>,
6771    ) -> Result<BlockId, LoweringError> {
6772        let Some(&step_info) = levels.first() else {
6773            return self.walk_stmt(body_stmt, cur);
6774        };
6775        let var = step_info.var;
6776        let entry = self.alloc_block();
6777        self.close(cur, Terminator::Fallthrough(entry));
6778        let counter = self.alloc_repeat_counter();
6779        let (count, first, by) = match step_info.fixed {
6780            Some((left, right)) => {
6781                let by: i64 = if left <= right { 1 } else { -1 };
6782                let n = (i64::from(left) - i64::from(right)).unsigned_abs() + 1;
6783                (RepeatCount::Const(n), i64::from(left) - by, by)
6784            }
6785            None if prefix.is_empty() => (RepeatCount::Size(arr), -1, 1),
6786            None => (
6787                RepeatCount::NestedSize {
6788                    arr,
6789                    path: prefix.clone(),
6790                },
6791                -1,
6792                1,
6793            ),
6794        };
6795        self.set_repeat_init(entry, counter, count);
6796        let header = self.alloc_block();
6797        let body = self.alloc_block();
6798        let exit = self.alloc_block();
6799        self.inlined_locals.push(var);
6800        if step_info.assoc {
6801            // Keys walk first/next, like a top-level associative array.
6802            self.blocks[entry.0 as usize]
6803                .bindings
6804                .push(ArgBinding::AssocKey {
6805                    var,
6806                    arr,
6807                    path: prefix.clone(),
6808                    next: false,
6809                });
6810        } else {
6811            self.blocks[entry.0 as usize]
6812                .bindings
6813                .push(ArgBinding::Const { var, value: first });
6814            self.blocks[header.0 as usize]
6815                .bindings
6816                .push(ArgBinding::Step { var, by });
6817        }
6818        self.close(
6819            entry,
6820            Terminator::Loop {
6821                header,
6822                body,
6823                exit,
6824                entry: header,
6825            },
6826        );
6827        self.close(
6828            header,
6829            Terminator::RepeatCheck {
6830                counter,
6831                then_blk: body,
6832                else_blk: exit,
6833            },
6834        );
6835        let latch = self.alloc_block();
6836        self.set_repeat_decrement(latch, counter);
6837        if step_info.assoc {
6838            self.blocks[latch.0 as usize]
6839                .bindings
6840                .push(ArgBinding::AssocKey {
6841                    var,
6842                    arr,
6843                    path: prefix.clone(),
6844                    next: true,
6845                });
6846        }
6847        self.close(latch, Terminator::Fallthrough(header));
6848        let outer = outer_exit.unwrap_or(exit);
6849        prefix.push(step_info);
6850        let body_exit = if levels.len() == 1 {
6851            self.walk_loop_body(body_stmt, body, latch, outer)?
6852        } else {
6853            self.walk_nest_levels(&levels[1..], prefix, arr, body_stmt, body, Some(outer))?
6854        };
6855        prefix.pop();
6856        self.close(body_exit, Terminator::Fallthrough(latch));
6857        Ok(exit)
6858    }
6859
6860    /// A `foreach` over a queue, dynamic or associative array whose body
6861    /// waits: a counted loop over the size the array has at loop entry (the
6862    /// `repeat` machinery, as for a fixed-size array). The index walks up
6863    /// from 0; an associative array's key starts at its first key and moves
6864    /// to the next one after each iteration (`first`/`next`, which is how
6865    /// LRM 1800-2023 §12.7.3 orders the keys).
6866    fn walk_collection_foreach(
6867        &mut self,
6868        stmt: Statement<'d>,
6869        cur: BlockId,
6870    ) -> Result<BlockId, LoweringError> {
6871        let arr = stmt.expressions()[0];
6872        let assoc = arr.expr_type().is_some_and(|t| t.is_associative_array());
6873        let dims = stmt.foreach_loop_dims();
6874        let body_stmt = stmt
6875            .body()
6876            .ok_or_else(|| LoweringError::malformed("ForeachLoop with no body statement"))?;
6877        // The inner dimensions are fixed-size ranges (`int d[][3]`): a
6878        // collection's elements are never themselves collections, so only
6879        // the outermost dimension is dynamic (LRM 1800-2023 §12.7.3).
6880        let mut rest = Vec::new();
6881        for d in dims.iter().skip(1) {
6882            let Some(v) = d.loop_var() else {
6883                continue;
6884            };
6885            let range = d.range().ok_or_else(|| {
6886                LoweringError::unsupported(
6887                    "foreach over a dimension of a queue or dynamic array's element that is \
6888                     itself a queue, dynamic or associative array",
6889                )
6890            })?;
6891            rest.push((v, i64::from(range.left), i64::from(range.right)));
6892        }
6893        let Some(var) = dims.first().and_then(|d| d.loop_var()) else {
6894            // Outermost index omitted: only the named inner dimensions run.
6895            return if rest.is_empty() {
6896                Ok(cur)
6897            } else {
6898                self.walk_foreach_nest(&rest, body_stmt, cur, None)
6899            };
6900        };
6901        let entry = self.alloc_block();
6902        self.close(cur, Terminator::Fallthrough(entry));
6903        let counter = self.alloc_repeat_counter();
6904        self.set_repeat_init(entry, counter, RepeatCount::Size(arr));
6905        let header = self.alloc_block();
6906        let body = self.alloc_block();
6907        let exit = self.alloc_block();
6908        self.inlined_locals.push(var);
6909        if assoc {
6910            self.blocks[entry.0 as usize]
6911                .bindings
6912                .push(ArgBinding::AssocKey {
6913                    var,
6914                    arr,
6915                    path: Vec::new(),
6916                    next: false,
6917                });
6918        } else {
6919            self.blocks[entry.0 as usize]
6920                .bindings
6921                .push(ArgBinding::Const { var, value: -1 });
6922            self.blocks[header.0 as usize]
6923                .bindings
6924                .push(ArgBinding::Step { var, by: 1 });
6925        }
6926        self.close(
6927            entry,
6928            Terminator::Loop {
6929                header,
6930                body,
6931                exit,
6932                entry: header,
6933            },
6934        );
6935        self.close(
6936            header,
6937            Terminator::RepeatCheck {
6938                counter,
6939                then_blk: body,
6940                else_blk: exit,
6941            },
6942        );
6943        let latch = self.alloc_block();
6944        self.set_repeat_decrement(latch, counter);
6945        if assoc {
6946            self.blocks[latch.0 as usize]
6947                .bindings
6948                .push(ArgBinding::AssocKey {
6949                    var,
6950                    arr,
6951                    path: Vec::new(),
6952                    next: true,
6953                });
6954        }
6955        self.close(latch, Terminator::Fallthrough(header));
6956        let body_exit = if rest.is_empty() {
6957            self.walk_loop_body(body_stmt, body, latch, exit)?
6958        } else {
6959            self.walk_foreach_nest(&rest, body_stmt, body, Some(exit))?
6960        };
6961        self.close(body_exit, Terminator::Fallthrough(latch));
6962        Ok(exit)
6963    }
6964
6965    /// The counted loops of [`Self::walk_foreach_loop`], `loops[0]`
6966    /// outermost: each is the `repeat` machinery counting the dimension's
6967    /// elements, its index variable walking from the left bound to the right
6968    /// bound (it starts one step before `left`; the header advances it
6969    /// before each iteration's count check).
6970    ///
6971    /// The nest is one loop to `break`/`continue` (LRM 1800-2023 §12.7.3,
6972    /// Verilator agrees): `continue` advances to the next element (the
6973    /// innermost dimension's latch) and `break` leaves the whole nest
6974    /// (`outer_exit`, the outermost dimension's exit).
6975    fn walk_foreach_nest(
6976        &mut self,
6977        loops: &[(Symbol<'d>, i64, i64)],
6978        body_stmt: Statement<'d>,
6979        cur: BlockId,
6980        outer_exit: Option<BlockId>,
6981    ) -> Result<BlockId, LoweringError> {
6982        let Some(&(var, left, right)) = loops.first() else {
6983            return self.walk_stmt(body_stmt, cur);
6984        };
6985        let step: i64 = if left <= right { 1 } else { -1 };
6986        let count = (left - right).unsigned_abs() + 1;
6987        // A fresh block for this loop's entry: `cur` may already carry
6988        // another counted loop's `repeat_init` (an enclosing foreach body).
6989        let entry = self.alloc_block();
6990        self.close(cur, Terminator::Fallthrough(entry));
6991        let counter = self.alloc_repeat_counter();
6992        self.set_repeat_init(entry, counter, RepeatCount::Const(count));
6993        let header = self.alloc_block();
6994        let body = self.alloc_block();
6995        let exit = self.alloc_block();
6996        self.inlined_locals.push(var);
6997        self.blocks[entry.0 as usize]
6998            .bindings
6999            .push(ArgBinding::Const {
7000                var,
7001                value: left - step,
7002            });
7003        self.blocks[header.0 as usize]
7004            .bindings
7005            .push(ArgBinding::Step { var, by: step });
7006        self.close(
7007            entry,
7008            Terminator::Loop {
7009                header,
7010                body,
7011                exit,
7012                entry: header,
7013            },
7014        );
7015        self.close(
7016            header,
7017            Terminator::RepeatCheck {
7018                counter,
7019                then_blk: body,
7020                else_blk: exit,
7021            },
7022        );
7023        // The latch counts the iteration down; `continue` comes here too.
7024        let latch = self.alloc_block();
7025        self.set_repeat_decrement(latch, counter);
7026        self.close(latch, Terminator::Fallthrough(header));
7027        let outer = outer_exit.unwrap_or(exit);
7028        let body_exit = if loops.len() == 1 {
7029            self.walk_loop_body(body_stmt, body, latch, outer)?
7030        } else {
7031            self.walk_foreach_nest(&loops[1..], body_stmt, body, Some(outer))?
7032        };
7033        self.close(body_exit, Terminator::Fallthrough(latch));
7034        Ok(exit)
7035    }
7036
7037    /// R-run Task 8 (PR-045/PR-006): `repeat(N) body;` when `body` contains
7038    /// a suspend point (the gate `walk_stmt`'s own dispatch already checked
7039    /// before calling this function). Mechanically similar to
7040    /// [`Self::walk_while_loop`] -- `header`'s own terminator carries the
7041    /// real per-iteration decision, `body`'s own terminator closes the back
7042    /// edge, `entry` is `header` (the counter is checked before the first
7043    /// iteration too, LRM: `repeat(0)` runs zero times) -- except the
7044    /// decision is [`Terminator::RepeatCheck`] against a compiler-
7045    /// synthesized [`RepeatCounterId`], not a re-evaluated user `Branch`
7046    /// condition, and `cur` (this loop's own entry block) additionally
7047    /// records a [`BasicBlock::repeat_init`] evaluating the count expression
7048    /// exactly once before ever reaching `header`. The loop body is walked
7049    /// recursively via the ordinary [`Self::walk_stmt`] entry point, so any
7050    /// suspend terminator nested inside it composes for free -- exactly the
7051    /// same reason `disable` "composes correctly across suspend boundaries
7052    /// for free" per `docs/design/process.md` §2, and the same recursive
7053    /// shape every other loop-walker in this file already uses.
7054    fn walk_repeat_loop(
7055        &mut self,
7056        stmt: Statement<'d>,
7057        cur: BlockId,
7058    ) -> Result<BlockId, LoweringError> {
7059        // R-run Task 4's `stmt_shared::lower_repeat_loop` already established
7060        // (and this file's own Task 4-era doc comments confirm) that
7061        // `RepeatLoop::condition()` is the count expression, not a boolean --
7062        // slang has no separate accessor for it, this AST node just reuses
7063        // the `condition()` grammar slot for `repeat`'s own count operand.
7064        let count_expr = stmt
7065            .condition()
7066            .ok_or_else(|| LoweringError::malformed("RepeatLoop with no count expression"))?;
7067        let body_stmt = stmt
7068            .body()
7069            .ok_or_else(|| LoweringError::malformed("RepeatLoop with no body statement"))?;
7070
7071        let counter = self.alloc_repeat_counter();
7072        self.set_repeat_init(cur, counter, RepeatCount::Expr(count_expr));
7073
7074        let header = self.alloc_block();
7075        let body = self.alloc_block();
7076        let exit = self.alloc_block();
7077        // 38.36.1.2: when first encountered and at every iteration (so one
7078        // more time than the loop body runs, the last one finding the count
7079        // exhausted -- the same shape the leaf lowering has).
7080        self.probe(header, stmt);
7081        self.close(
7082            cur,
7083            Terminator::Loop {
7084                header,
7085                body,
7086                exit,
7087                entry: header,
7088            },
7089        );
7090        self.close(
7091            header,
7092            Terminator::RepeatCheck {
7093                counter,
7094                then_blk: body,
7095                else_blk: exit,
7096            },
7097        );
7098        // The latch counts the iteration down; `continue` comes here too.
7099        let latch = self.alloc_block();
7100        self.set_repeat_decrement(latch, counter);
7101        self.close(latch, Terminator::Fallthrough(header));
7102        let body_exit = self.walk_loop_body(body_stmt, body, latch, exit)?;
7103        self.close(body_exit, Terminator::Fallthrough(latch));
7104        Ok(exit)
7105    }
7106}
7107
7108/// Whether `node` (a statement kept as one straight-line leaf) contains a
7109/// jump out of itself into the CFG around it: a `break`/`continue` not
7110/// inside a loop of its own (when `in_loop`, an enclosing CFG loop exists)
7111/// or a `return` (when `in_task`, the statement is part of an inlined task
7112/// body).
7113fn escapes(node: SemNode<'_>, in_loop: bool, in_task: bool) -> bool {
7114    let mut stack = vec![(node, false)];
7115    while let Some((n, nested_loop)) = stack.pop() {
7116        let mut inner = nested_loop;
7117        if let Some(s) = n.as_statement() {
7118            let kind = s.kind();
7119            if matches!(kind, StatementKind::Break | StatementKind::Continue)
7120                && in_loop
7121                && !nested_loop
7122            {
7123                return true;
7124            }
7125            if kind == StatementKind::Return && in_task {
7126                return true;
7127            }
7128            if matches!(
7129                kind,
7130                StatementKind::ForLoop
7131                    | StatementKind::WhileLoop
7132                    | StatementKind::DoWhileLoop
7133                    | StatementKind::ForeverLoop
7134                    | StatementKind::ForeachLoop
7135                    | StatementKind::RepeatLoop
7136            ) {
7137                inner = true;
7138            }
7139        }
7140        stack.extend(n.children().into_iter().map(|c| (c, inner)));
7141    }
7142    false
7143}
7144
7145/// Whether `stmt` is a statement a VPI `cbStmt` callback fires for once, just
7146/// before it executes (IEEE 1800-2023 Table 38-6): the blocks, `if`, `case`,
7147/// `wait`, the procedural assignments / `force` / `release` / `disable` /
7148/// event triggers, task and system-task calls, and the delay and event
7149/// controls. This mirrors the statements the VPI model gives an object of
7150/// those types. The loops (`while`/`for`/`repeat`/`forever`) fire per
7151/// iteration instead and are not covered here.
7152pub fn is_entry_probe_kind(stmt: &Statement<'_>) -> bool {
7153    match stmt.kind() {
7154        StatementKind::Block
7155        | StatementKind::Conditional
7156        | StatementKind::Case
7157        | StatementKind::PatternCase
7158        | StatementKind::Wait
7159        | StatementKind::Disable
7160        | StatementKind::EventTrigger
7161        | StatementKind::ProceduralAssign
7162        | StatementKind::ProceduralDeassign => true,
7163        StatementKind::ExpressionStatement => stmt.expr().is_some_and(|e| {
7164            matches!(
7165                e.kind(),
7166                ExpressionKind::Assignment | ExpressionKind::Call | ExpressionKind::UnaryOp
7167            )
7168        }),
7169        StatementKind::Timed => {
7170            stmt.timing_control().map(|t| t.kind()) != Some(TimingControlKind::RepeatedEvent)
7171        }
7172        // Not in Table 38-6, or a loop (probed per iteration): no entry probe.
7173        StatementKind::Invalid
7174        | StatementKind::Empty
7175        | StatementKind::List
7176        | StatementKind::VariableDeclaration
7177        | StatementKind::Return
7178        | StatementKind::Continue
7179        | StatementKind::Break
7180        | StatementKind::DisableFork
7181        | StatementKind::ForLoop
7182        | StatementKind::RepeatLoop
7183        | StatementKind::ForeachLoop
7184        | StatementKind::WhileLoop
7185        | StatementKind::DoWhileLoop
7186        | StatementKind::ForeverLoop
7187        | StatementKind::ImmediateAssertion
7188        | StatementKind::ConcurrentAssertion
7189        | StatementKind::WaitFork
7190        | StatementKind::WaitOrder
7191        | StatementKind::RandCase
7192        | StatementKind::RandSequence
7193        | StatementKind::ProceduralChecker => false,
7194        other => unreachable!("statement kind {other:?} is newer than this classification"),
7195    }
7196}
7197
7198thread_local! {
7199    /// The text of each source file of the design (as it was parsed, so
7200    /// offsets of a [`sv_lang::SourceLoc`] index it), which `disable_label`
7201    /// reads a `disable` statement's target from, by file name. One file
7202    /// registered without a name ([`set_source_text`]) answers for every
7203    /// location.
7204    static DISABLE_SOURCE: std::cell::RefCell<Vec<(Option<String>, std::rc::Rc<str>)>> =
7205        const { std::cell::RefCell::new(Vec::new()) };
7206}
7207
7208/// Registers the text of the source file statements are elaborated from,
7209/// for `disable label;` (see [`disable_label`]); `None` clears it.
7210pub fn set_source_text(text: Option<&str>) {
7211    DISABLE_SOURCE.with(|s| {
7212        *s.borrow_mut() = text
7213            .map(|t| (None, std::rc::Rc::from(t)))
7214            .into_iter()
7215            .collect();
7216    });
7217    scan_disable_names(text.into_iter());
7218}
7219
7220thread_local! {
7221    /// What the registered source text names (see [`scan_disable_names`]):
7222    /// every `disable <name>;` target, and every task name.
7223    static DISABLE_NAMES: std::cell::RefCell<(
7224        std::collections::HashSet<String>,
7225        std::collections::HashSet<String>,
7226    )> = std::cell::RefCell::new(Default::default());
7227}
7228
7229/// Records which names a `disable <name>;` of the design targets, and which
7230/// names are tasks, from the registered source text (the binding cannot read a
7231/// `disable` target, see the module doc). Over-approximate on purpose (a word
7232/// in a comment counts): a name here only makes the builder keep a scope exit
7233/// for it ([`CfgBuilder::scope_chain`]) or accept it as a task of the design.
7234fn scan_disable_names<'a>(texts: impl Iterator<Item = &'a str>) {
7235    let mut labels = std::collections::HashSet::new();
7236    let mut tasks = std::collections::HashSet::new();
7237    let mut fn_labels = std::collections::HashSet::new();
7238    let ident = |c: u8| c == b'_' || c == b'$' || c.is_ascii_alphanumeric();
7239    for text in texts {
7240        let b = text.as_bytes();
7241        let word_at = |mut i: usize| -> Option<(&str, usize)> {
7242            while i < b.len() && b[i].is_ascii_whitespace() {
7243                i += 1;
7244            }
7245            let start = i;
7246            while i < b.len() && ident(b[i]) {
7247                i += 1;
7248            }
7249            (i > start).then(|| (&text[start..i], i))
7250        };
7251        let mut i = 0;
7252        // Inside a `function` body (a DPI prototype or `pure`/`extern`
7253        // declaration has none, and ends at its `;`).
7254        let (mut in_fn, mut proto) = (false, false);
7255        while i < b.len() {
7256            if !ident(b[i]) {
7257                if b[i] == b';' {
7258                    proto = false;
7259                }
7260                i += 1;
7261                continue;
7262            }
7263            let start = i;
7264            while i < b.len() && ident(b[i]) {
7265                i += 1;
7266            }
7267            let boundary = start == 0 || (b[start - 1] != b'.' && b[start - 1] != b'`');
7268            match &text[start..i] {
7269                "import" | "export" | "extern" | "pure" if boundary => proto = true,
7270                "function" if boundary => in_fn = !proto,
7271                "endfunction" if boundary => in_fn = false,
7272                "disable" if boundary => {
7273                    // `disable a.b.c;` (9.6.2) names its last segment.
7274                    // A generate-block index (`g[0].bk`) is part of a segment.
7275                    let skip_idx = |mut e: usize| -> usize {
7276                        loop {
7277                            let t = text[e..].trim_start();
7278                            let lead = text[e..].len() - t.len();
7279                            let Some(inner) = t.strip_prefix('[') else {
7280                                return e;
7281                            };
7282                            match inner.find(']') {
7283                                Some(c)
7284                                    if inner[..c].trim().bytes().all(|d| d.is_ascii_digit()) =>
7285                                {
7286                                    e += lead + 1 + c + 1;
7287                                }
7288                                _ => return e,
7289                            }
7290                        }
7291                    };
7292                    if let Some((mut name, e0)) = word_at(i)
7293                        && name != "fork"
7294                    {
7295                        let mut end = skip_idx(e0);
7296                        while text[end..].trim_start().starts_with('.')
7297                            && let Some((next, e)) = word_at(
7298                                end + (text[end..].len() - text[end..].trim_start().len()) + 1,
7299                            )
7300                        {
7301                            name = next;
7302                            end = skip_idx(e);
7303                        }
7304                        if text[end..].trim_start().starts_with(';') {
7305                            labels.insert(name.to_string());
7306                            if in_fn {
7307                                fn_labels.insert(name.to_string());
7308                            }
7309                            // The whole dotted path too ([`CfgBuilder::name_path`]).
7310                            let full: String = text[i..end]
7311                                .chars()
7312                                .filter(|c| !c.is_ascii_whitespace())
7313                                .collect();
7314                            // Every dotted suffix too: `disable u1.main.inner;`
7315                            // names the block `inner` of `main` by the path
7316                            // below the instance (9.6.2).
7317                            let mut rest = full.as_str();
7318                            while let Some((_, tail)) = rest.split_once('.') {
7319                                if tail.contains('.') {
7320                                    labels.insert(tail.to_string());
7321                                }
7322                                rest = tail;
7323                            }
7324                            if full.contains('.') {
7325                                labels.insert(full);
7326                            }
7327                        }
7328                    }
7329                }
7330                "task" if boundary => {
7331                    if let Some((mut name, end)) = word_at(i) {
7332                        if matches!(name, "automatic" | "static")
7333                            && let Some((n, _)) = word_at(end)
7334                        {
7335                            name = n;
7336                        }
7337                        tasks.insert(name.to_string());
7338                    }
7339                }
7340                _ => {}
7341            }
7342        }
7343    }
7344    DISABLE_NAMES.with(|n| *n.borrow_mut() = (labels, tasks));
7345    FN_DISABLE_LABELS.with(|n| *n.borrow_mut() = fn_labels);
7346}
7347
7348thread_local! {
7349    /// The targets of the `disable <name>;` statements written inside a
7350    /// `function` body (see [`scan_disable_names`]).
7351    static FN_DISABLE_LABELS: std::cell::RefCell<std::collections::HashSet<String>> =
7352        std::cell::RefCell::new(Default::default());
7353}
7354
7355/// Whether the design has a `disable <name>;` inside a function body: such a
7356/// function can end a block of its caller (LRM 9.6.2, 35.9), which the call
7357/// sites check for ([`SwitchOn::FnDisable`]). Designs without one pay nothing.
7358pub fn has_function_disables() -> bool {
7359    FN_DISABLE_LABELS.with(|n| !n.borrow().is_empty())
7360}
7361
7362/// Whether a function body of the design has `disable <label>;`.
7363pub fn is_function_disable_label(label: &str) -> bool {
7364    FN_DISABLE_LABELS.with(|n| n.borrow().contains(label))
7365}
7366
7367/// Whether some `disable` statement of the design names `label`.
7368fn is_disabled_somewhere(label: &str) -> bool {
7369    DISABLE_NAMES.with(|n| n.borrow().0.contains(label))
7370}
7371
7372/// Every dotted `disable a.b.c;` target (and dotted suffix of one) the design
7373/// writes, sorted.
7374pub fn dotted_disable_labels() -> Vec<String> {
7375    let mut v: Vec<String> = DISABLE_NAMES.with(|n| {
7376        n.borrow()
7377            .0
7378            .iter()
7379            .filter(|l| l.contains('.'))
7380            .cloned()
7381            .collect()
7382    });
7383    v.sort();
7384    v
7385}
7386
7387/// Whether `label` is the name of a task of the design.
7388pub fn is_task_name(label: &str) -> bool {
7389    DISABLE_NAMES.with(|n| n.borrow().1.contains(label))
7390}
7391
7392/// Registers the text of every source file of a multi-file design, each
7393/// under the file name its locations resolve to ([`set_source_session`]).
7394pub fn set_source_texts(files: Vec<(String, String)>) {
7395    scan_disable_names(files.iter().map(|(_, t)| t.as_str()));
7396    DISABLE_SOURCE.with(|s| {
7397        *s.borrow_mut() = files
7398            .into_iter()
7399            .map(|(name, text)| (Some(name), std::rc::Rc::from(text)))
7400            .collect();
7401    });
7402}
7403
7404/// Whether two file names denote the same file (equal, or the same file on
7405/// disk).
7406fn same_file(a: &str, b: &str) -> bool {
7407    a == b
7408        || std::fs::canonicalize(a)
7409            .ok()
7410            .is_some_and(|x| std::fs::canonicalize(b).ok() == Some(x))
7411}
7412
7413/// The registered text holding `at`: the unnamed one if that is what was
7414/// registered, else the named file `at` resolves to (through the session
7415/// registered by [`set_source_session`]).
7416fn source_text_at(at: sv_lang::SourceLoc) -> Option<std::rc::Rc<str>> {
7417    let files = DISABLE_SOURCE.with(|s| s.borrow().clone());
7418    if let [(None, text)] = files.as_slice() {
7419        return Some(text.clone());
7420    }
7421    let file = SOURCE_SESSION
7422        .with(|s| s.borrow().as_ref().and_then(|se| se.resolve_location(at)))?
7423        .file;
7424    files
7425        .into_iter()
7426        .find(|(n, _)| n.as_deref().is_some_and(|n| same_file(n, &file)))
7427        .map(|(_, text)| text)
7428}
7429
7430thread_local! {
7431    /// The main source file's name as given (`$info`'s `file:line`).
7432    static SOURCE_NAME: std::cell::RefCell<Option<String>> =
7433        const { std::cell::RefCell::new(None) };
7434}
7435
7436/// Registers the main source file's name (see [`source_location`]).
7437pub fn set_source_name(name: Option<&str>) {
7438    SOURCE_NAME.with(|s| *s.borrow_mut() = name.map(str::to_owned));
7439}
7440
7441/// The source text of `span`, from the file text registered by
7442/// [`set_source_text`] / [`set_source_texts`] (`None` if that file's text is
7443/// not registered, e.g. a span in an `` `include `` d file).
7444pub fn source_text_range(span: sv_lang::SourceSpan) -> Option<String> {
7445    source_text_at(span.start)?
7446        .get(span.start.offset as usize..span.end.offset as usize)
7447        .map(str::to_owned)
7448}
7449
7450thread_local! {
7451    /// The session the design was parsed with: resolves a source location
7452    /// to its file and line (included files and `` `line `` directives
7453    /// included).
7454    static SOURCE_SESSION: std::cell::RefCell<Option<sv_lang::Session>> =
7455        const { std::cell::RefCell::new(None) };
7456}
7457
7458/// Registers the session the design was parsed with (see
7459/// [`source_location`]); `None` clears it.
7460pub fn set_source_session(session: Option<sv_lang::Session>) {
7461    SOURCE_SESSION.with(|s| *s.borrow_mut() = session);
7462}
7463
7464/// `file:line` of `loc`, as slang's source manager reports it (honoring
7465/// `` `include `` and `` `line ``) through the session registered by
7466/// [`set_source_session`] -- the main source file under the name it was
7467/// given ([`set_source_name`]); without a session, counted in the main
7468/// source file registered by [`set_source_name`]/[`set_source_text`].
7469pub fn source_location(loc: sv_lang::SourceLoc) -> Option<String> {
7470    if let Some(at) = SOURCE_SESSION.with(|s| {
7471        s.borrow()
7472            .as_ref()
7473            .and_then(|session| session.resolve_location(loc))
7474    }) {
7475        let main = SOURCE_NAME.with(|s| s.borrow().clone());
7476        let file = match main {
7477            Some(m) if same_file(&at.file, &m) => m,
7478            _ => at.file,
7479        };
7480        return Some(format!("{file}:{}", at.line));
7481    }
7482    let name = SOURCE_NAME.with(|s| s.borrow().clone())?;
7483    let text = source_text_at(loc)?;
7484    let head = text.get(..(loc.offset as usize).min(text.len()))?;
7485    let line = head.bytes().filter(|&b| b == b'\n').count() + 1;
7486    Some(format!("{name}:{line}"))
7487}
7488
7489/// The segments of a hierarchical `disable a.b.c;` target (LRM 9.6.2), read
7490/// from the statement's source text like [`disable_label`]; `None` for a
7491/// simple or unreadable target.
7492pub fn disable_path(stmt: &Statement<'_>) -> Option<Vec<String>> {
7493    let text = source_text_range(stmt.source_range())?;
7494    let rest = text.trim().strip_prefix("disable")?;
7495    if !rest.starts_with(char::is_whitespace) {
7496        return None;
7497    }
7498    let label: String = rest
7499        .trim()
7500        .trim_end_matches(';')
7501        .chars()
7502        .filter(|c| !c.is_whitespace())
7503        .collect();
7504    let segs: Vec<String> = label.split('.').map(str::to_string).collect();
7505    // A generate-block segment carries its index (`g[0]`).
7506    let ident = |t: &str| {
7507        let t = match t.find('[') {
7508            Some(b)
7509                if t.ends_with(']')
7510                    && t[b + 1..t.len() - 1].bytes().all(|d| d.is_ascii_digit())
7511                    && b + 2 < t.len() =>
7512            {
7513                &t[..b]
7514            }
7515            Some(_) => return false,
7516            None => t,
7517        };
7518        !t.is_empty()
7519            && t.chars().enumerate().all(|(i, c)| {
7520                c == '_' || c.is_ascii_alphabetic() || (i > 0 && (c.is_ascii_digit() || c == '$'))
7521            })
7522    };
7523    (segs.len() > 1 && segs.iter().all(|t| ident(t))).then_some(segs)
7524}
7525
7526/// The name `disable` acts on in a statement the CFG builder kept: the plain
7527/// label, or the last segment of a hierarchical `a.b.c` the builder resolved to
7528/// a block of another process ([`disable_path`]).
7529pub fn disable_leaf_label(stmt: &Statement<'_>) -> Result<String, LoweringError> {
7530    match disable_path(stmt).and_then(|p| p.last().cloned()) {
7531        Some(leaf) => Ok(leaf),
7532        None => disable_label(stmt),
7533    }
7534}
7535
7536/// The target name of a `disable label;` statement. The pinned sv-lang
7537/// binding exposes no `DisableStatement::target` (see the module doc), so
7538/// it is read from the statement's own source span in the text registered
7539/// by [`set_source_text`] -- accepted only when that span reads exactly
7540/// `disable <identifier> ;`, so a span into another file (an `` `include ``)
7541/// is a named error, never a misread label.
7542pub fn disable_label(stmt: &Statement<'_>) -> Result<String, LoweringError> {
7543    let span = stmt.source_range();
7544    let text = source_text_range(span);
7545    let unreadable = || {
7546        LoweringError::unsupported(
7547            "disable statement whose target cannot be read (no registered source text, or \
7548             the statement is not in a registered source file)",
7549        )
7550    };
7551    let text = text.ok_or_else(unreadable)?;
7552    let rest = text.trim().strip_prefix("disable").ok_or_else(unreadable)?;
7553    let label = rest.trim().trim_end_matches(';').trim();
7554    let ident = !label.is_empty()
7555        && label.chars().enumerate().all(|(i, c)| {
7556            c == '_' || c.is_ascii_alphabetic() || (i > 0 && (c.is_ascii_digit() || c == '$'))
7557        });
7558    if !ident || !rest.starts_with(char::is_whitespace) {
7559        let hier = if label.contains('.') {
7560            "; disabling a task or interface method by hierarchical name would have to \
7561             end every activation of it in other processes (IEEE 1800-2023 9.6.2)"
7562        } else {
7563            ""
7564        };
7565        return Err(LoweringError::unsupported(format!(
7566            "disable of `{label}` (only a simple block name is supported{hier})"
7567        )));
7568    }
7569    Ok(label.to_string())
7570}
7571
7572/// Whether `node`'s subtree contains a `disable label;` statement, in itself
7573/// or in the body of a task it calls (an inlined task's `disable` may target
7574/// the caller's block, LRM 9.6.2).
7575fn contains_disable_through_calls(node: SemNode<'_>) -> bool {
7576    let mut visiting = std::collections::HashSet::new();
7577    let mut stack = vec![node];
7578    while let Some(n) = stack.pop() {
7579        if n.as_statement()
7580            .is_some_and(|s| s.kind() == StatementKind::Disable)
7581        {
7582            return true;
7583        }
7584        if let Some(e) = n.as_expression()
7585            && let Some(callee) = user_task_callee(&e)
7586            && let Some(body) = proc_body(&callee)
7587            && visiting.insert(callee.uid())
7588        {
7589            stack.push(body.as_sem_node());
7590        }
7591        stack.extend(live_children(&n));
7592    }
7593    false
7594}
7595
7596/// Whether `node`'s subtree holds a `fork`/`join*` block, in itself or in the
7597/// body of a task it calls.
7598fn contains_fork_through_calls(node: SemNode<'_>) -> bool {
7599    let mut visiting = std::collections::HashSet::new();
7600    let mut stack = vec![node];
7601    while let Some(n) = stack.pop() {
7602        if n.as_statement().is_some_and(|s| {
7603            matches!(
7604                s.block_kind(),
7605                Some(
7606                    StatementBlockKind::JoinAll
7607                        | StatementBlockKind::JoinAny
7608                        | StatementBlockKind::JoinNone
7609                )
7610            )
7611        }) {
7612            return true;
7613        }
7614        if let Some(e) = n.as_expression()
7615            && let Some(callee) = user_task_callee(&e)
7616            && let Some(body) = proc_body(&callee)
7617            && visiting.insert(callee.uid())
7618        {
7619            stack.push(body.as_sem_node());
7620        }
7621        stack.extend(live_children(&n));
7622    }
7623    false
7624}
7625
7626/// Every `StatementKind` variant [`CfgBuilder::walk_stmt`] treats as ordinary
7627/// straight-line code (PR-006's own acceptance text) -- i.e. every variant
7628/// that is not one of the 5 explicitly-named statement kinds
7629/// (`List`/`Block`/`Conditional`/`Timed`/the 5 suspend-rejecting kinds) it
7630/// handles directly. Kept as a `matches!` (not a literal `match`, see
7631/// `walk_stmt`'s doc comment) enumerating all 22 remaining variants of
7632/// today's pinned `StatementKind::COUNT == 32`.
7633/// Task 1 removed `ForLoop`/`WhileLoop`/`DoWhileLoop`/`ForeverLoop`/
7634/// `ForeachLoop`/`Disable` from this set. The five loop kinds now dispatch
7635/// to dedicated handlers (PR-045's `Terminator::Loop`) instead of being
7636/// treated as opaque leaves; `Disable` also gets a dedicated `walk_stmt` arm
7637/// (a jump to its named block's exit -- see this file's module doc comment's
7638/// `disable label;` section), never the "push as ordinary code" treatment
7639/// this function's callers give everything below. `RepeatLoop` (a plain statement-level `repeat(N)
7640/// stmt;`, distinct from the `RepeatedEvent` *timing control*) stays
7641/// ordinary -- PR-045's own requirement text names exactly `for`/`while`/
7642/// `do-while`/`forever`/`foreach`, not `repeat`.
7643fn is_ordinary_statement_kind(kind: StatementKind) -> bool {
7644    matches!(
7645        kind,
7646        StatementKind::Empty
7647            | StatementKind::ExpressionStatement
7648            | StatementKind::VariableDeclaration
7649            | StatementKind::Return
7650            | StatementKind::Continue
7651            | StatementKind::Break
7652            | StatementKind::Case
7653            | StatementKind::PatternCase
7654            | StatementKind::RepeatLoop
7655            | StatementKind::ImmediateAssertion
7656            | StatementKind::ConcurrentAssertion
7657            | StatementKind::ProceduralAssign
7658            | StatementKind::ProceduralDeassign
7659            | StatementKind::RandCase
7660            | StatementKind::RandSequence
7661            | StatementKind::ProceduralChecker
7662    )
7663}
7664
7665/// R-run Task 1: `true` iff `stmt` (already known to be a `StatementKind::
7666/// ExpressionStatement`) is a bare `$finish`/`$finish(n)`/`$stop`/`$fatal`
7667/// system-call TASK statement -- the exact same `ExpressionKind::Call` +
7668/// `is_system_call() == Some(true)` + `system_subroutine().known_name_id()`
7669/// recognition `vj-codegen::process_lower::lower_system_call_statement`
7670/// already uses for its own dispatch (mirrored, not re-derived differently:
7671/// both call sites must agree on what counts as one of these three calls).
7672/// `$fatal`'s own optional finish_number/message arguments and `$stop`'s own
7673/// interactive-debugger-break runtime semantics are unaffected by this CFG-
7674/// level check -- it only ever answers "does the calling process's own
7675/// control flow end here", never anything about the call's own argument
7676/// list or its eventual codegen lowering (`$stop`/`$fatal` still lower to a
7677/// named `LoweringError` in `process_lower.rs` today, exactly as before this
7678/// task; this predicate firing for them one file earlier changes nothing
7679/// observable about that until a later task actually lowers them).
7680fn is_finish_or_stop_call(stmt: &Statement<'_>) -> bool {
7681    let Some(e) = stmt.expr() else {
7682        return false;
7683    };
7684    if e.kind() != ExpressionKind::Call || e.is_system_call() != Some(true) {
7685        return false;
7686    }
7687    let Some(sub) = e.system_subroutine() else {
7688        return false;
7689    };
7690    matches!(
7691        sub.known_name_id(),
7692        KnownSystemName::Finish | KnownSystemName::Stop | KnownSystemName::Fatal
7693    )
7694}
7695
7696/// The `unique`/`unique0`/`priority` qualifier on an `if` or `case` statement
7697/// (LRM 1800-2023 §12.4.2, §12.5.3), `None` for an unqualified statement or
7698/// any other kind. A `case ... matches` ([`StatementKind::PatternCase`])
7699/// carries its qualifier under its own accessor.
7700pub fn violation_check(stmt: &Statement<'_>) -> Option<UniquePriorityCheck> {
7701    let check = if stmt.kind() == StatementKind::PatternCase {
7702        stmt.pattern_case_check()
7703    } else {
7704        stmt.check()
7705    };
7706    match check {
7707        Some(UniquePriorityCheck::None) | None => None,
7708        Some(check) => Some(check),
7709    }
7710}
7711
7712/// The `if` / `else if` series a `unique`/`unique0`/`priority` qualifier on
7713/// the first `if` applies to (§12.4.2: "the entire series of if-else-if
7714/// conditions"): the condition and then-statement of each link, in source
7715/// order, and the final `else` statement if there is one. A nested
7716/// `else if` carries no qualifier of its own (the grammar has none); an
7717/// `else` holding a qualified `if` of its own is the final `else` and a
7718/// separate statement.
7719pub struct IfChain<'d> {
7720    /// The `if` statement of each link (its predicate is `links[i]
7721    /// .conditions()`: plain, `&&&`-chained, or pattern-matching, §12.6).
7722    pub links: Vec<Statement<'d>>,
7723    pub thens: Vec<Statement<'d>>,
7724    pub otherwise: Option<Statement<'d>>,
7725}
7726
7727/// Peels `Invalid` wrappers off `stmt` (IEEE 1800-2023 12.4/12.5).
7728///
7729/// slang suppresses the error of a construct inside a statically dead `if`
7730/// or `case` arm (e.g. a reversed range select `v[7:8]` of a descending
7731/// vector under a false parameter) but demotes every enclosing statement to
7732/// `Invalid`. Such wrappers nest (an `Invalid` body wraps a `Block` that wraps
7733/// an `Invalid` wrapping the `Conditional`), so this unwraps recursively via
7734/// `Statement::invalid_child`. An `Invalid` with no recoverable child is
7735/// returned unchanged, so the caller's loud "Invalid statement" refusal still
7736/// fires for it.
7737pub fn unwrap_invalid<'d>(mut stmt: Statement<'d>) -> Statement<'d> {
7738    while stmt.kind() == StatementKind::Invalid {
7739        match stmt.invalid_child() {
7740            Some(child) => stmt = child,
7741            None => break,
7742        }
7743    }
7744    stmt
7745}
7746
7747/// The child statements of a `List`/`Block`, with each statement slang
7748/// demoted to `Invalid` (a statically dead `if`/`case` arm it could not
7749/// bind) unwrapped to the real one. `Statement::statements` skips `Invalid`
7750/// children, which would silently drop the live statement (LRM 12.4/12.5).
7751pub fn live_statements<'d>(stmt: &Statement<'d>) -> Vec<Statement<'d>> {
7752    stmt.as_sem_node()
7753        .children_with_invalid()
7754        .into_iter()
7755        .filter_map(|n| n.as_statement())
7756        .map(unwrap_invalid)
7757        .collect()
7758}
7759
7760/// The children of `node` for a whole-subtree walk, with each `Invalid`
7761/// statement or expression (slang's demotion of a construct inside a
7762/// statically dead `if`/`case` arm it could not bind, LRM 12.4/12.5) replaced
7763/// by the real node it wraps. `SemNode::children` skips `Invalid` children,
7764/// so a call or read inside the live arm of such a statement would otherwise
7765/// be invisible to every discovery walk (called-subroutine discovery, write
7766/// sets, unsupported-construct scans).
7767pub fn live_children<'d>(node: &SemNode<'d>) -> Vec<SemNode<'d>> {
7768    node.children_with_invalid()
7769        .into_iter()
7770        .map(|n| {
7771            if let Some(s) = n.as_statement() {
7772                return unwrap_invalid(s).as_sem_node();
7773            }
7774            if let Some(mut e) = n.as_expression() {
7775                while e.kind() == ExpressionKind::Invalid
7776                    && let Some(c) = e.invalid_child()
7777                {
7778                    e = c;
7779                }
7780                return e.as_sem_node();
7781            }
7782            n
7783        })
7784        .collect()
7785}
7786
7787/// The body statement of a procedural block with any `Invalid` wrappers from
7788/// a statically dead arm peeled off (see [`unwrap_invalid`]). Every pass that
7789/// inspects a process's head timing control, read set or statements must go
7790/// through this rather than `Symbol::body`, so they all see the same live
7791/// body (otherwise an `always @*` loses its sensitivity).
7792pub fn proc_body<'d>(block: &Symbol<'d>) -> Option<Statement<'d>> {
7793    block.body().map(unwrap_invalid)
7794}
7795
7796/// The `StatementBlock` scopes whose locals `stmt` (a `Block`) declares.
7797///
7798/// Normally that is `Statement::block_symbol()`. slang never attaches the
7799/// symbol to a block that failed to bind (`Statement::bindBlock` returns the
7800/// `Invalid` wrapper before setting `blockSymbol`), so a `begin`/`end` that
7801/// holds a statically dead `if`/`case` arm it could not bind (LRM 12.4/12.5)
7802/// is recovered through `invalid_child` with no symbol, and its locals would
7803/// be invisible. For such a block the scopes are recovered from the variables
7804/// the live subtree references: the `StatementBlock` parents of its
7805/// `NamedValue` symbols. That can include an enclosing or nested block's
7806/// scope, which is harmless for the union-style local collections that call
7807/// this; a local no live statement references is not recovered (it has no
7808/// observable use).
7809pub fn block_scopes<'d>(stmt: &Statement<'d>) -> Vec<Symbol<'d>> {
7810    if let Some(bs) = stmt.block_symbol() {
7811        return vec![bs];
7812    }
7813    let node = stmt.as_sem_node();
7814    if stmt.kind() != StatementKind::Block || !contains_invalid(node) {
7815        return Vec::new();
7816    }
7817    let mut out: Vec<Symbol<'d>> = Vec::new();
7818    let mut seen = HashSet::new();
7819    let mut stack = vec![node];
7820    while let Some(n) = stack.pop() {
7821        if let Some(e) = n.as_expression()
7822            && crate::symid::is_value_name(e.kind())
7823            && let Some(sym) = e.referenced_symbol()
7824            && let Some(parent) = sym.parent()
7825            && parent.kind() == SymbolKind::StatementBlock
7826            && seen.insert(parent.uid())
7827        {
7828            out.push(parent);
7829        }
7830        stack.extend(live_children(&n));
7831    }
7832    out
7833}
7834
7835/// The name of a `begin`/`end` or `fork`/`join` block statement (LRM 9.3.4),
7836/// empty when unnamed.
7837///
7838/// Normally `Statement::block_symbol()`'s name. A block that failed to bind
7839/// (a statically dead arm slang could not bind, LRM 12.4) carries no symbol,
7840/// so its name would be lost and `disable blk` (LRM 9.6.2) refused. For such
7841/// a block the name is read back from the statement's own source span in the
7842/// text registered by [`set_source_text`]: either the `begin : name` form or
7843/// a `name : begin` statement label. Anything else reads as unnamed.
7844pub fn block_name(stmt: &Statement<'_>) -> String {
7845    if let Some(bs) = stmt.block_symbol() {
7846        return bs.name().to_string();
7847    }
7848    if stmt.kind() != StatementKind::Block || !contains_invalid(stmt.as_sem_node()) {
7849        return String::new();
7850    }
7851    let Some(text) = source_text_range(stmt.source_range()) else {
7852        return String::new();
7853    };
7854    let ident = |s: &str| -> Option<(String, String)> {
7855        let s = s.trim_start();
7856        let n = s
7857            .char_indices()
7858            .take_while(|&(i, c)| {
7859                c == '_' || c.is_ascii_alphabetic() || (i > 0 && (c.is_ascii_digit() || c == '$'))
7860            })
7861            .count();
7862        (n > 0).then(|| (s[..n].to_string(), s[n..].to_string()))
7863    };
7864    let Some((first, rest)) = ident(&text) else {
7865        return String::new();
7866    };
7867    if matches!(first.as_str(), "begin" | "fork") {
7868        // `begin : name`; a bare `begin` followed by a statement is unnamed.
7869        return match rest.trim_start().strip_prefix(':').and_then(ident) {
7870            Some((name, _)) => name,
7871            None => String::new(),
7872        };
7873    }
7874    // `name : begin`.
7875    match rest.trim_start().strip_prefix(':').and_then(ident) {
7876        Some((kw, _)) if matches!(kw.as_str(), "begin" | "fork") => first,
7877        _ => String::new(),
7878    }
7879}
7880
7881/// Whether any node under `node` is an `Invalid` statement or expression.
7882fn contains_invalid(node: SemNode<'_>) -> bool {
7883    let is_invalid = node
7884        .as_statement()
7885        .is_some_and(|s| s.kind() == StatementKind::Invalid)
7886        || node
7887            .as_expression()
7888            .is_some_and(|e| e.kind() == ExpressionKind::Invalid);
7889    is_invalid
7890        || node
7891            .children_with_invalid()
7892            .into_iter()
7893            .any(contains_invalid)
7894}
7895
7896/// For a plain `case` whose selector and labels are all compile-time
7897/// constants and whose arms include an `Invalid` one (a statically dead arm
7898/// slang could not bind, LRM 12.5): `Some(arm)` names the one statement that
7899/// can ever run (`None` inside: no arm matches and there is no `default`).
7900/// `None` for any case this does not apply to (non-constant, `casez`/`casex`/
7901/// `inside`, a call in a label, or no `Invalid` arm), which lowers as usual.
7902pub fn constant_case_live_arm<'d>(stmt: &Statement<'d>) -> Option<Option<Statement<'d>>> {
7903    fn has_call(e: Expression<'_>) -> bool {
7904        e.kind() == ExpressionKind::Call || e.operands().into_iter().any(has_call)
7905    }
7906    if stmt.kind() != StatementKind::Case
7907        || stmt.case_condition() != Some(sv_lang::CaseStatementCondition::Normal)
7908        || violation_check(stmt).is_some()
7909    {
7910        return None;
7911    }
7912    let items = stmt.case_items();
7913    let default = stmt.default_case();
7914    if !items
7915        .iter()
7916        .map(|i| i.stmt())
7917        .chain(default)
7918        .any(|a| contains_invalid(a.as_sem_node()))
7919    {
7920        return None;
7921    }
7922    let sel = stmt.expr()?;
7923    if has_call(sel) {
7924        return None;
7925    }
7926    let sv = sel.constant_value()?.as_i64()?;
7927    for item in &items {
7928        for label in item.expressions() {
7929            if has_call(label) {
7930                return None;
7931            }
7932            if label.constant_value()?.as_i64()? == sv {
7933                return Some(Some(item.stmt()));
7934            }
7935        }
7936    }
7937    Some(default)
7938}
7939
7940/// The fixed truth of a plain `if`'s predicate when it is a compile-time
7941/// constant with no function call in it (`if (PARAM > 0)`): `Some(taken)`
7942/// names the one arm that can ever run, so the other need not be lowered
7943/// (LRM 1800-2023 12.4; the dead arm may be legal yet unlowerable once
7944/// its constants fold, e.g. an out-of-range part-select, 11.5.1). A call is
7945/// excluded because the evaluator would run it at compile time, skipping
7946/// its side effects. `None` for any other `if`.
7947pub fn constant_if_arm(stmt: &Statement<'_>) -> Option<bool> {
7948    fn has_call(e: Expression<'_>) -> bool {
7949        e.kind() == ExpressionKind::Call || e.operands().into_iter().any(has_call)
7950    }
7951    let conditions = stmt.conditions();
7952    if stmt.kind() != StatementKind::Conditional
7953        || conditions.len() != 1
7954        || conditions[0].pattern().is_some()
7955        || violation_check(stmt).is_some()
7956    {
7957        return None;
7958    }
7959    let cond = conditions[0].expr();
7960    if has_call(cond) {
7961        return None;
7962    }
7963    cond.constant_value()
7964        .and_then(|c| c.as_i64())
7965        .map(|c| c != 0)
7966}
7967
7968/// Whether `stmt` is an `if` whose predicate is not a single plain
7969/// expression: a pattern match (`e matches p`) or an `&&&` chain (§12.6.1).
7970pub fn is_clause_if(stmt: &Statement<'_>) -> bool {
7971    if stmt.kind() != StatementKind::Conditional {
7972        return false;
7973    }
7974    let conditions = stmt.conditions();
7975    conditions.len() != 1 || conditions[0].pattern().is_some()
7976}
7977
7978/// The links an `if` dispatches over: the whole `else if` series of a
7979/// `unique`/`unique0`/`priority` `if`, else the one `if` alone (its `else`,
7980/// if any, is the `otherwise` statement).
7981pub fn if_series<'d>(head: Statement<'d>) -> Result<IfChain<'d>, &'static str> {
7982    if violation_check(&head).is_some() {
7983        return if_chain(head);
7984    }
7985    Ok(IfChain {
7986        links: vec![head],
7987        thens: vec![head.then_branch().ok_or("an `if` with no then-branch")?],
7988        otherwise: head.else_branch(),
7989    })
7990}
7991
7992/// Collects the [`IfChain`] of the `if` statement `head`.
7993pub fn if_chain<'d>(head: Statement<'d>) -> Result<IfChain<'d>, &'static str> {
7994    let mut chain = IfChain {
7995        links: Vec::new(),
7996        thens: Vec::new(),
7997        otherwise: None,
7998    };
7999    let mut link = head;
8000    loop {
8001        chain.links.push(link);
8002        chain
8003            .thens
8004            .push(link.then_branch().ok_or("an `if` with no then-branch")?);
8005        match link.else_branch() {
8006            Some(next)
8007                if next.kind() == StatementKind::Conditional
8008                    && violation_check(&next).is_none() =>
8009            {
8010                link = next;
8011            }
8012            other => {
8013                chain.otherwise = other;
8014                return Ok(chain);
8015            }
8016        }
8017    }
8018}
8019
8020fn first_expression_child<'d>(tc: &TimingControl<'d>) -> Option<Expression<'d>> {
8021    tc.children().into_iter().find_map(|n| n.as_expression())
8022}
8023
8024/// Task 6 (CG-081): the AST-derived read-set of `body` -- every distinct
8025/// (by referenced-symbol identity) `NamedValue` expression `body`'s subtree
8026/// reads, excluding a plain `NamedValue` LHS of a top-level `Assignment`
8027/// (mirrors `vj-codegen::process_lower::external_refs_in_segment`'s
8028/// identical "read vs write position" split, at the granularity of one
8029/// guarded statement rather than a whole `Segment`).
8030///
8031/// **Deliberately AST-only, not `AnalyzedProcedure::
8032/// implicit_event_read_sets()`:** `sv-lang` exposes exactly this oracle
8033/// (`slang::analysis::AnalyzedProcedure::getImplicitEventReadSets`), but it
8034/// needs a live `sv_lang::Analysis` handle, and no `build_cfg` call site in
8035/// this tree (`vj::run`, `aot.rs`, `pipeline.rs`, `frontend.rs`, this
8036/// crate's own `liveness.rs` tests) threads one through today -- adding that
8037/// parameter would be a signature change to `build_cfg` reaching every one
8038/// of those call sites, including `vj::run.rs`, which this wave's
8039/// binding rules forbid risking (wave-B-runtime is concurrently landing
8040/// there). This AST-only walk is a real, if narrower, substitute: an
8041/// over-approximation (a name reached only through a path this walk cannot
8042/// prove is write-only) is a safe, LRM-permitted implicit-sensitivity
8043/// superset (an extra spurious wake, never a missed one) -- never a
8044/// correctness bug the way under-approximating would be. See this file's
8045/// module doc comment's Task 6 section and this task's own deviations for
8046/// the full accounting.
8047fn implicit_comb_read_set<'d>(body: &Statement<'d>) -> Vec<Expression<'d>> {
8048    let mut order = Vec::new();
8049    let mut seen = HashSet::new();
8050    let mut stack = vec![body.as_sem_node()];
8051    while let Some(n) = stack.pop() {
8052        // A subtree slang demoted to `Invalid` inside a statically dead arm
8053        // is invisible to `children`; recover it (its reads are a harmless
8054        // superset of the live ones, LRM 9.4.2.2).
8055        if let Some(st) = n.as_statement()
8056            && st.kind() == StatementKind::Invalid
8057        {
8058            if let Some(c) = st.invalid_child() {
8059                stack.push(c.as_sem_node());
8060            }
8061            continue;
8062        }
8063        if let Some(e) = n.as_expression()
8064            && e.kind() == ExpressionKind::Invalid
8065        {
8066            if let Some(c) = e.invalid_child() {
8067                stack.push(c.as_sem_node());
8068            }
8069            continue;
8070        }
8071        if let Some(e) = n.as_expression() {
8072            if crate::symid::is_value_name(e.kind()) {
8073                if let Some(sym) = e.referenced_symbol()
8074                    && seen.insert(sym.uid())
8075                {
8076                    order.push(e);
8077                }
8078                continue;
8079            }
8080            if e.kind() == ExpressionKind::Assignment {
8081                // Skip only the LHS's own bare `NamedValue` node of a PLAIN
8082                // (`=`/`<=`) assignment (never a "read" for implicit-
8083                // sensitivity purposes, whether blocking or nonblocking) --
8084                // a compound lvalue (a bit-select, concatenation, ...) is
8085                // walked normally instead, so e.g. `arr[i] = x`'s index
8086                // expression `i` (and `arr`'s own identity) are still
8087                // collected, matching the "safe over-approximation" posture
8088                // above.
8089                //
8090                // A COMPOUND assignment (`+=`, `-=`, `&=`, ...) is never
8091                // desugared by slang -- `right()` holds only the RHS
8092                // operand (e.g. `a` for `q += a`), never `q + a` -- so a
8093                // bare `NamedValue` LHS genuinely is also a read there (per
8094                // IEEE 1800's implicit-sensitivity rule: a variable is
8095                // excluded from the inferred `@*` list only when it is
8096                // written but never read, which a compound assignment's LHS
8097                // is not). Detect this via `is_compound_assignment()` and
8098                // walk the LHS unconditionally in that case.
8099                let lhs_is_read = e.is_compound_assignment() == Some(true);
8100                if let Some(lhs) = e.left()
8101                    && (lhs_is_read || !crate::symid::is_value_name(lhs.kind()))
8102                {
8103                    stack.push(lhs.as_sem_node());
8104                }
8105                if let Some(rhs) = e.right() {
8106                    stack.push(rhs.as_sem_node());
8107                }
8108                continue;
8109            }
8110        }
8111        stack.extend(n.children_with_invalid());
8112    }
8113    order
8114}
8115
8116/// LRM 6.14: a chandle "shall not be used ... in sensitivity lists or event
8117/// expressions"; slang does not diagnose it, so refuse it by name here
8118/// instead of waiting on a value that can never be observed to change.
8119fn reject_chandle_event(signal: &Expression<'_>) -> Result<(), LoweringError> {
8120    if signal.expr_type().is_some_and(|t| t.is_chandle()) {
8121        return Err(LoweringError::unsupported(
8122            "a chandle in an event expression (illegal, LRM 1800-2023 6.14)".to_string(),
8123        ));
8124    }
8125    Ok(())
8126}
8127
8128/// The operands `(memory, input_terms, output_terms)` of `stmt` when it is a
8129/// `$async$<and|or|nand|nor>$<array|plane>` call (LRM 20.16), else `None`.
8130pub fn async_pla_call<'d>(stmt: &Statement<'d>) -> Option<Vec<Expression<'d>>> {
8131    let e = stmt.expr()?;
8132    if e.kind() != ExpressionKind::Call || e.is_system_call() != Some(true) {
8133        return None;
8134    }
8135    let name = e.system_subroutine()?.name();
8136    let rest = name.strip_prefix("$async$")?;
8137    let (logic, format) = rest.split_once('$')?;
8138    (matches!(logic, "and" | "or" | "nand" | "nor") && matches!(format, "array" | "plane"))
8139        .then(|| e.operands())
8140}
8141
8142/// What an `$async$...` PLA call re-evaluates on: the personality memory and
8143/// everything the input terms read, never the output terms it writes.
8144fn async_pla_read_set<'d>(stmt: &Statement<'d>) -> Vec<Expression<'d>> {
8145    let ops = async_pla_call(stmt).unwrap_or_default();
8146    let root = |x: Expression<'d>| {
8147        let x = if x.kind() == ExpressionKind::Assignment {
8148            x.left().unwrap_or(x)
8149        } else {
8150            x
8151        };
8152        crate::symid::is_value_name(x.kind())
8153            .then(|| x.referenced_symbol())
8154            .flatten()
8155            .map(|s| (x, s.uid()))
8156    };
8157    let out_uid = ops.get(2).and_then(|o| root(*o)).map(|(_, u)| u);
8158    let mut reads: Vec<Expression<'d>> = implicit_comb_read_set(stmt)
8159        .into_iter()
8160        .filter(|r| r.referenced_symbol().map(|s| s.uid()) != out_uid)
8161        .collect();
8162    if let Some((mem, uid)) = ops.first().and_then(|o| root(*o))
8163        && !reads
8164            .iter()
8165            .any(|r| r.referenced_symbol().map(|s| s.uid()) == Some(uid.clone()))
8166    {
8167        reads.push(mem);
8168    }
8169    reads
8170}
8171
8172/// Task 3 (CG-170): extracts a `SignalEventControl`'s optional `iff`
8173/// condition. **Verified live** against the pinned `sv-lang = "=0.1.9"`
8174/// source (no `.iffCondition` accessor exists in this binding at all): per
8175/// slang's own `SignalEventControl::visitExprs` (`include/slang/ast/
8176/// TimingControl.h`: `expr.visit(visitor); if (iffCondition)
8177/// iffCondition->visit(visitor);`), which is what backs `children()`
8178/// (`CApiAst.cpp`'s generic `SemChildCollector`), a `SignalEvent`
8179/// `TimingControl` node's `children()` returns exactly 1 entry (the event
8180/// expression only) with no `iff`, or exactly 2 (event expression, then
8181/// `iff`, in that order) when one is present. Extraction is therefore
8182/// purely positional: `len() == 2`'s `[1]` is `iff`, `None` otherwise.
8183fn extract_iff<'d>(tc: &TimingControl<'d>) -> Option<Expression<'d>> {
8184    let children = tc.children();
8185    if children.len() == 2 {
8186        children[1].as_expression()
8187    } else {
8188        None
8189    }
8190}
8191
8192/// The pass and fail statements of an immediate assertion when one of them
8193/// waits (LRM 16.3: they are ordinary statements, so `#d`/`@e`/`wait` are
8194/// legal there), else `None` (the assertion stays a straight-line leaf). A
8195/// deferred assertion's action is a subroutine call that is queued (16.4), so
8196/// a waiting one is refused by name.
8197fn suspending_assertion_actions<'d>(
8198    stmt: &Statement<'d>,
8199) -> Option<(Option<Statement<'d>>, Option<Statement<'d>>)> {
8200    let pass = stmt.immediate_assertion_if_true();
8201    let fail = stmt.immediate_assertion_if_false();
8202    let waits = |s: &Option<Statement<'d>>| {
8203        s.as_ref()
8204            .is_some_and(|s| contains_suspend_trigger(s.as_sem_node()))
8205    };
8206    (waits(&pass) || waits(&fail)).then_some((pass, fail))
8207}
8208
8209/// The expression an immediate assertion checks.
8210pub fn assertion_cond<'d>(stmt: &Statement<'d>) -> Option<Expression<'d>> {
8211    stmt.as_sem_node()
8212        .children()
8213        .into_iter()
8214        .find_map(|n| n.as_expression())
8215}
8216
8217/// Whether `stmt` is an `expect` statement (LRM 1800-2023 §16.17): slang models
8218/// it as a concurrent assertion of kind `Expect`.
8219fn is_expect(stmt: &Statement<'_>) -> bool {
8220    stmt.kind() == StatementKind::ConcurrentAssertion
8221        && stmt.assertion_kind() == Some(sv_lang::AssertionKind::Expect)
8222}
8223
8224/// Generic depth-first scan (mirrors `vj-codegen::module::
8225/// reject_unsupported_in_body`'s pattern) for any statement kind
8226/// [`CfgBuilder::walk_stmt`] treats as suspend-triggering, wherever it sits
8227/// inside `node`'s subtree. Used only by the "ordinary straight-line" match
8228/// arm: a construct this task does not decompose (loops, `case`, ...) is
8229/// safe to keep as one opaque leaf `Statement` only as long as none of its
8230/// descendants secretly suspends.
8231fn contains_suspend_trigger(node: SemNode<'_>) -> bool {
8232    contains_suspend_trigger_in(node, &mut std::collections::HashSet::new(), true, true)
8233}
8234
8235/// `contains_suspend_trigger`, also looking through calls to user tasks
8236/// (whose bodies are inlined when they can suspend); `visiting` breaks
8237/// recursion through recursive tasks. `by_request` says whether a
8238/// `p.await();` / `p.suspend();` statement counts: not inside a function
8239/// body, which cannot be inlined as a suspending task (a `suspend()` there
8240/// takes effect at the caller's next blocking point instead).
8241///
8242/// `disable_is_trigger`: whether a `disable label;` counts (it is a jump the
8243/// CFG decomposes, but never itself suspends the process).
8244fn contains_suspend_trigger_in(
8245    node: SemNode<'_>,
8246    visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8247    by_request: bool,
8248    disable_is_trigger: bool,
8249) -> bool {
8250    let mut stack = vec![node];
8251    while let Some(n) = stack.pop() {
8252        if let Some(e) = n.as_expression()
8253            && let Some(callee) = user_task_callee(&e)
8254            && is_suspending_virtual(&callee)
8255        {
8256            return true;
8257        }
8258        if let Some(e) = n.as_expression()
8259            && e.kind() == ExpressionKind::Assignment
8260            && e.assignment_timing().is_some()
8261            && (e.is_nonblocking() != Some(true)
8262                || e.assignment_timing()
8263                    .and_then(|t| t.as_timing_control())
8264                    .is_some_and(|tc| tc.kind() != TimingControlKind::Delay))
8265        {
8266            // `lhs = #d rhs;` / `lhs = @(e) rhs;` / `q <= @(e) rhs;` (LRM 9.4.5,
8267            // 10.4.2): the assignment itself waits (`walk_intra_assignment_timing`,
8268            // `walk_nba_event`), so a task holding one suspends and is inlined --
8269            // otherwise `disable task;` of another process cannot reach it (9.6.2).
8270            return true;
8271        }
8272        if let Some(e) = n.as_expression() {
8273            // An interface `extern` call runs the module-side
8274            // implementations (LRM 25.7): their bodies are what can suspend.
8275            // A call to an `extern forkjoin` task nothing implements is a
8276            // run-time error the CFG reports (`Terminator::Switch`).
8277            let callees: Vec<Symbol<'_>> = match crate::netlist::itf::call_extern_targets(&e) {
8278                Some(t) if t.impls.is_empty() => return true,
8279                Some(t) => t.impls,
8280                None => user_task_callee(&e).into_iter().collect(),
8281            };
8282            for callee in callees {
8283                // A function with an `output`/`inout`/`ref` event formal is
8284                // inlined like a suspending one (`has_event_out_formal`).
8285                if has_event_out_formal(&callee) {
8286                    return true;
8287                }
8288                if let Some(body) = proc_body(&callee)
8289                    && visiting.insert(callee.uid())
8290                    && contains_suspend_trigger_in(
8291                        body.as_sem_node(),
8292                        visiting,
8293                        by_request
8294                            && callee.subroutine_kind() == Some(sv_lang::SubroutineKind::Task),
8295                        // A function is compiled, never inlined: its own
8296                        // `disable blk;` is a branch there (codegen `func`).
8297                        disable_is_trigger
8298                            && callee.subroutine_kind() == Some(sv_lang::SubroutineKind::Task),
8299                    )
8300                {
8301                    return true;
8302                }
8303            }
8304        }
8305        if let Some(s) = n.as_statement() {
8306            let kind = s.kind();
8307            if by_request
8308                && kind == StatementKind::ExpressionStatement
8309                && is_process_by_request_stmt(&s)
8310            {
8311                return true;
8312            }
8313            let is_join_block = kind == StatementKind::Block
8314                && matches!(
8315                    s.block_kind(),
8316                    Some(
8317                        StatementBlockKind::JoinAll
8318                            | StatementBlockKind::JoinAny
8319                            | StatementBlockKind::JoinNone
8320                    )
8321                );
8322            if is_join_block
8323                || is_expect(&s)
8324                || matches!(
8325                    kind,
8326                    StatementKind::Timed
8327                        | StatementKind::Wait
8328                        | StatementKind::WaitFork
8329                        | StatementKind::WaitOrder
8330                        | StatementKind::EventTrigger
8331                        | StatementKind::DisableFork
8332                )
8333                // `disable label;` is a jump in the CFG (see this file's
8334                // module doc comment) -- one hidden inside an opaque
8335                // "ordinary" leaf (e.g. a `case` branch) forces that leaf to
8336                // be decomposed too.
8337                || (disable_is_trigger && kind == StatementKind::Disable)
8338            {
8339                return true;
8340            }
8341            // A static or procedural concurrent assertion (16.14) is evaluated by the
8342            // assertion engine, never by the process that reaches it: a task its
8343            // property calls from a sequence match item (16.11) runs as a thread of
8344            // its own, so it suspends no process here. Only the pass/fail action
8345            // statements run in the hosting process and can hold a suspend point.
8346            if kind == StatementKind::ConcurrentAssertion {
8347                stack.extend(
8348                    live_children(&n)
8349                        .into_iter()
8350                        .filter(|c| c.as_statement().is_some()),
8351                );
8352                continue;
8353            }
8354        }
8355        stack.extend(live_children(&n));
8356    }
8357    false
8358}
8359
8360/// Whether `task` can call itself ([`task_reaches`]), memoized per task in
8361/// `vt` when there is one.
8362fn task_reaches_itself<'a>(task: &Symbol<'a>, vt: Option<&VirtualTasks<'a>>) -> bool {
8363    let uid = task.uid();
8364    if let Some(vt) = vt
8365        && let Some(r) = vt.self_reach.borrow().get(&uid)
8366    {
8367        return *r;
8368    }
8369    let r = task_reaches(task, &uid, vt, &mut std::collections::HashSet::new());
8370    if let Some(vt) = vt {
8371        vt.self_reach.borrow_mut().insert(uid, r);
8372    }
8373    r
8374}
8375
8376/// Whether `sub`'s body calls `target` (directly or through the user tasks
8377/// and functions it calls). A call of a virtual method reaches every
8378/// implementation `vt` knows of (LRM 1800-2023 8.20), so recursion through
8379/// dynamic dispatch (`a.f()` -> override -> `a.f()` on a child) is found.
8380fn task_reaches<'a>(
8381    sub: &Symbol<'a>,
8382    target: &crate::symid::SymbolId,
8383    vt: Option<&VirtualTasks<'a>>,
8384    visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8385) -> bool {
8386    let Some(body) = proc_body(sub) else {
8387        return false;
8388    };
8389    let mut stack = vec![body.as_sem_node()];
8390    while let Some(n) = stack.pop() {
8391        if let Some(e) = n.as_expression()
8392            && let Some(callee) = user_task_callee(&e)
8393        {
8394            let mut callees = vec![callee];
8395            // `super.m()` is not virtual (LRM 8.15): it never reaches the
8396            // overrides, so it must not be seen as recursion through them.
8397            if let Some(vt) = vt
8398                && !is_explicit_super(e)
8399                && (callee
8400                    .subroutine_flags()
8401                    .contains(sv_lang::MethodFlags::VIRTUAL)
8402                    || callee.subroutine_override().is_some())
8403            {
8404                callees.extend(vt.implementations(&callee).into_iter().map(|(_, s)| s));
8405            }
8406            for c in callees {
8407                if c.uid() == *target {
8408                    return true;
8409                }
8410                if visiting.insert(c.uid()) && task_reaches(&c, target, vt, visiting) {
8411                    return true;
8412                }
8413            }
8414        }
8415        stack.extend(live_children(&n));
8416    }
8417    false
8418}
8419
8420/// Whether `node`'s subtree calls a subroutine that is not a system task.
8421fn contains_subroutine_call(node: SemNode<'_>) -> bool {
8422    let mut stack = vec![node];
8423    while let Some(n) = stack.pop() {
8424        if let Some(e) = n.as_expression()
8425            && e.kind() == ExpressionKind::Call
8426            && e.is_system_call() != Some(true)
8427        {
8428            return true;
8429        }
8430        stack.extend(live_children(&n));
8431    }
8432    false
8433}
8434
8435/// The callee of a call to a user (non-system, non-DPI) task or function.
8436fn user_task_callee<'d>(e: &Expression<'d>) -> Option<Symbol<'d>> {
8437    if e.kind() != ExpressionKind::Call || e.is_system_call() == Some(true) {
8438        return None;
8439    }
8440    let callee = e.call_subroutine()?;
8441    if callee
8442        .subroutine_flags()
8443        .contains(sv_lang::MethodFlags::DPI_IMPORT)
8444    {
8445        return None;
8446    }
8447    Some(callee)
8448}
8449
8450/// The callee of `e` when it is a call to a user task whose body can
8451/// suspend -- such a call is inlined into the calling process's CFG.
8452fn suspending_task_callee<'d>(e: &Expression<'d>) -> Option<Symbol<'d>> {
8453    let callee = user_task_callee(e)?;
8454    subroutine_can_suspend(&callee).then_some(callee)
8455}
8456
8457/// The calls to user functions that can suspend (a `fork` in the body)
8458/// under `node`, innermost (argument) calls first.
8459fn collect_suspending_function_calls<'d>(node: SemNode<'d>, out: &mut Vec<Expression<'d>>) {
8460    for c in node.children() {
8461        collect_suspending_function_calls(c, out);
8462    }
8463    if let Some(e) = node.as_expression()
8464        && let Some(callee) = user_task_callee(&e)
8465        && callee.subroutine_kind() == Some(sv_lang::SubroutineKind::Function)
8466        && subroutine_can_suspend(&callee)
8467    {
8468        out.push(e);
8469    }
8470}
8471
8472/// Whether `sub` has an `output`/`inout`/`ref` `event` formal: its handle is
8473/// copied back to the actual (LRM 1800-2023 15.5.5.1), which only an inlined
8474/// body can do, so every call to it is inlined (value functions through
8475/// `hoist_suspending_function_calls`).
8476fn has_event_out_formal(sub: &Symbol<'_>) -> bool {
8477    sub.subroutine_arguments().any(|a| {
8478        a.formal_argument_direction() != sv_lang::ArgumentDirection::In
8479            && a.value_type().is_some_and(|t| t.is_event())
8480    })
8481}
8482
8483/// The key under which the code generator finds the capture temporary that
8484/// holds the value of the inlined call `call` ([`ArgBinding::CallResult`]).
8485pub fn call_result_key(call: &Expression<'_>) -> crate::symid::SymbolId {
8486    let at = call.source_range().start;
8487    crate::symid::SymbolId::synthetic_tagged(&format!("callret.{}.{}", at.buffer, at.offset))
8488}
8489
8490/// Whether `sub`'s body (or a task it calls) contains a suspend point --
8491/// every call to such a task is inlined into the calling process's CFG
8492/// instead of being compiled as a function.
8493pub fn subroutine_can_suspend(sub: &Symbol<'_>) -> bool {
8494    if is_suspending_virtual(sub) {
8495        return true;
8496    }
8497    let Some(body) = proc_body(sub) else {
8498        return false;
8499    };
8500    // An `output`/`inout`/`ref` event formal is copied back as a handle
8501    // (LRM 1800-2023 15.5.5.1), which only an inlined task can do.
8502    if has_event_out_formal(sub) {
8503        return true;
8504    }
8505    let mut visiting = std::collections::HashSet::new();
8506    visiting.insert(sub.uid());
8507    let by_request = sub.subroutine_kind() == Some(sv_lang::SubroutineKind::Task);
8508    contains_suspend_trigger_in(body.as_sem_node(), &mut visiting, by_request, by_request)
8509        || (sub.subroutine_kind() == Some(sv_lang::SubroutineKind::Task)
8510            && task_stages_nba(sub, &mut std::collections::HashSet::new()))
8511}
8512
8513/// Whether the task `sub` (or a task it calls) runs a nonblocking assignment
8514/// (LRM 1800-2023 10.4.2). Staging an NBA needs the calling process's
8515/// scheduler handle, which a compiled function does not have, so such a task
8516/// is inlined into each calling process like a suspending one -- it just
8517/// never reaches a suspend point.
8518fn task_stages_nba(
8519    sub: &Symbol<'_>,
8520    visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8521) -> bool {
8522    let Some(body) = proc_body(sub) else {
8523        return false;
8524    };
8525    let mut stack = vec![body.as_sem_node()];
8526    while let Some(n) = stack.pop() {
8527        if let Some(e) = n.as_expression() {
8528            if e.kind() == ExpressionKind::Assignment && e.is_nonblocking() == Some(true) {
8529                return true;
8530            }
8531            if let Some(callee) = user_task_callee(&e)
8532                && callee.subroutine_kind() == Some(sv_lang::SubroutineKind::Task)
8533                && visiting.insert(callee.uid())
8534                && task_stages_nba(&callee, visiting)
8535            {
8536                return true;
8537            }
8538        }
8539        stack.extend(live_children(&n));
8540    }
8541    false
8542}
8543
8544/// The method name when `callee` is a method of the built-in `std::process`
8545/// class (LRM 9.7): the class named `process` whose scope chain reaches the
8546/// `std` package -- scope identity, as `vj-codegen`'s
8547/// `is_builtin_std_class` does (a user class `process` is not it).
8548pub fn std_process_method(callee: &Symbol<'_>) -> Option<String> {
8549    std_class_method(callee, "process")
8550}
8551
8552/// The method name when `callee` is a method of the built-in `std` package
8553/// class named `class_name` (`process`, `semaphore`, `mailbox`): scope
8554/// identity, so a user class of the same name is not it.
8555pub fn std_class_method(callee: &Symbol<'_>, class_name: &str) -> Option<String> {
8556    let class = callee.parent()?;
8557    if !matches!(
8558        class.kind(),
8559        SymbolKind::ClassType | SymbolKind::GenericClassDef
8560    ) || class.name() != class_name
8561    {
8562        return None;
8563    }
8564    let mut scope = class.parent();
8565    while let Some(s) = scope {
8566        if s.kind() == SymbolKind::Package && s.name() == "std" {
8567            return Some(callee.name().to_string());
8568        }
8569        scope = s.parent();
8570    }
8571    None
8572}
8573
8574/// Whether event expression `e` names a CLASS event (LRM 1800-2023 15.5): an
8575/// instance `event` property (`m_event`, `obj.m_event`, `h[k].raised`), or an
8576/// `event` local or formal declared inside a class method. Such an event is a
8577/// runtime object (`sched::class_event`), not a row of a module's event pool.
8578/// Conversions are looked through; `null` is no class event by itself.
8579pub fn is_class_event_expr(e: &Expression<'_>) -> bool {
8580    let mut e = *e;
8581    while e.kind() == ExpressionKind::Conversion
8582        && let Some(inner) = e.conversion_operand()
8583    {
8584        e = inner;
8585    }
8586    if !e.expr_type().is_some_and(|t| t.is_event()) {
8587        return false;
8588    }
8589    let sym = if e.kind() == ExpressionKind::MemberAccess {
8590        e.member_symbol()
8591    } else if matches!(
8592        e.kind(),
8593        ExpressionKind::NamedValue | ExpressionKind::HierarchicalValue
8594    ) {
8595        e.referenced_symbol()
8596    } else {
8597        None
8598    };
8599    let Some(sym) = sym else {
8600        return false;
8601    };
8602    if sym.kind() == SymbolKind::ClassProperty {
8603        return true;
8604    }
8605    // A local/formal: the nearest enclosing subroutine is a class method.
8606    let mut scope = sym.parent();
8607    while let Some(s) = scope {
8608        if s.kind() == SymbolKind::Subroutine {
8609            return s.parent().is_some_and(|p| {
8610                matches!(
8611                    p.kind(),
8612                    SymbolKind::ClassType | SymbolKind::GenericClassDef
8613                )
8614            });
8615        }
8616        if matches!(
8617            s.kind(),
8618            SymbolKind::ClassType | SymbolKind::GenericClassDef
8619        ) {
8620            return false;
8621        }
8622        scope = s.parent();
8623    }
8624    false
8625}
8626
8627/// The class events `tc` waits on when it is `@e` on a class event or an
8628/// `@(e1 or e2 ...)` list with class events (no edge on a class term): waits
8629/// that queue through the runtime ([`is_class_event_expr`]). Empty when `tc`
8630/// waits on no class event. A list that also has signal or module-event terms
8631/// is [`class_event_wait_is_mixed`]; an edge on a class term is a named error,
8632/// and so is an `iff` on a signal term of a mixed list (its guard would also
8633/// gate the class wake). A class term's `iff` is judged after the wake
8634/// ([`class_event_term_iffs`]).
8635pub fn class_event_wait_terms<'d>(
8636    tc: &TimingControl<'d>,
8637) -> Result<Vec<Expression<'d>>, LoweringError> {
8638    let kind = tc.kind();
8639    if kind == TimingControlKind::SignalEvent {
8640        let Some(sig) = first_expression_child(tc) else {
8641            return Ok(Vec::new());
8642        };
8643        if !is_class_event_expr(&sig) {
8644            return Ok(Vec::new());
8645        }
8646        if tc.edge() != EdgeKind::None {
8647            return Err(LoweringError::unsupported(
8648                "an edge event control on a class `event` (LRM 1800-2023 15.5)",
8649            ));
8650        }
8651        return Ok(vec![sig]);
8652    }
8653    if kind == TimingControlKind::EventList {
8654        let mut terms = Vec::new();
8655        let mut plain_iff = false;
8656        for child in tc.children() {
8657            let Some(sub) = child.as_timing_control() else {
8658                continue;
8659            };
8660            match first_expression_child(&sub) {
8661                Some(sig)
8662                    if sub.kind() == TimingControlKind::SignalEvent
8663                        && is_class_event_expr(&sig) =>
8664                {
8665                    if sub.edge() != EdgeKind::None {
8666                        return Err(LoweringError::unsupported(
8667                            "an edge term on a class `event` in an `@(a or b)` event list (LRM 1800-2023 15.5)",
8668                        ));
8669                    }
8670                    terms.push(sig);
8671                }
8672                _ => {
8673                    if extract_iff(&sub).is_some() {
8674                        plain_iff = true;
8675                    }
8676                }
8677            }
8678        }
8679        if plain_iff && !terms.is_empty() {
8680            return Err(LoweringError::unsupported(
8681                "an `iff` on a signal term of an `@(a or b)` event list that also has a class `event` term (the guard would gate the class wake too; LRM 1800-2023 9.4.2.3, 15.5)",
8682            ));
8683        }
8684        return Ok(terms);
8685    }
8686    Ok(Vec::new())
8687}
8688
8689/// The `iff` condition of each class-event term of the list `tc`, aligned
8690/// with [`class_event_wait_terms`] (the runtime queues a process on a term by
8691/// its index there). Empty unless `tc` is an event list.
8692pub fn class_event_term_iffs<'d>(tc: &TimingControl<'d>) -> Vec<Option<Expression<'d>>> {
8693    let mut out = Vec::new();
8694    if tc.kind() != TimingControlKind::EventList {
8695        return out;
8696    }
8697    for child in tc.children() {
8698        let Some(sub) = child.as_timing_control() else {
8699            continue;
8700        };
8701        if sub.kind() == TimingControlKind::SignalEvent
8702            && first_expression_child(&sub).is_some_and(|sig| is_class_event_expr(&sig))
8703        {
8704            out.push(extract_iff(&sub));
8705        }
8706    }
8707    out
8708}
8709
8710/// Whether `tc` is an `@(a or b)` event list that mixes class `event` terms
8711/// with signal or module-event terms (LRM 1800-2023 9.4.2.1, 15.5): such a
8712/// wait is both a class-event queue entry and a sensitivity-table wait.
8713pub fn class_event_wait_is_mixed(tc: &TimingControl<'_>) -> bool {
8714    if tc.kind() != TimingControlKind::EventList {
8715        return false;
8716    }
8717    let (mut class, mut plain) = (false, false);
8718    for child in tc.children() {
8719        let Some(sub) = child.as_timing_control() else {
8720            continue;
8721        };
8722        match first_expression_child(&sub) {
8723            Some(sig)
8724                if sub.kind() == TimingControlKind::SignalEvent && is_class_event_expr(&sig) =>
8725            {
8726                class = true;
8727            }
8728            _ => plain = true,
8729        }
8730    }
8731    class && plain
8732}
8733
8734/// Whether `stmt` is `p.await();`, `p.suspend();`, a blocking
8735/// `semaphore.get(...)` or a mailbox `put(...)`/`get(...)`/`peek(...)` as a
8736/// statement of its own (not nested in a larger expression): the `std` calls
8737/// that are a suspend point, because the caller may stop there.
8738fn is_process_by_request_stmt(stmt: &Statement<'_>) -> bool {
8739    let Some(e) = stmt.expr() else {
8740        return false;
8741    };
8742    if e.kind() != ExpressionKind::Call || e.is_system_call() == Some(true) {
8743        return false;
8744    }
8745    let Some(callee) = e.call_subroutine() else {
8746        return false;
8747    };
8748    std_process_method(&callee).is_some_and(|m| m == "await" || m == "suspend")
8749        || std_class_method(&callee, "semaphore").is_some_and(|m| m == "get")
8750        || std_class_method(&callee, "mailbox")
8751            .is_some_and(|m| matches!(m.as_str(), "put" | "get" | "peek"))
8752}
8753
8754/// Whether `stmt` is a mailbox `get(x);` / `peek(x);`: the blocking calls whose
8755/// result is delivered after the process resumes (the CFG emits the statement
8756/// again after its suspend point). A `put` finishes in its one call.
8757fn is_mailbox_two_phase_stmt(stmt: &Statement<'_>) -> bool {
8758    let Some(e) = stmt.expr() else {
8759        return false;
8760    };
8761    e.kind() == ExpressionKind::Call
8762        && e.is_system_call() != Some(true)
8763        && e.call_subroutine()
8764            .and_then(|callee| std_class_method(&callee, "mailbox"))
8765            .is_some_and(|m| m == "get" || m == "peek")
8766}
8767
8768/// Whether a call to `sub` can end the calling process from inside it: its
8769/// body, or a subroutine it calls, runs `$finish`/`$stop`/`$fatal` or a
8770/// task-form `$cast` that can fail, uses a
8771/// virtual interface (a `null` one is a fatal error, LRM 1800-2023 §25.9),
8772/// or calls a DPI import (which may call back into an export that does).
8773/// A call to a virtual method counts if ANY implementation in its override
8774/// chain can abort ([`mark_aborting_virtual`]: which override runs is only
8775/// known at run time), and a `new` expression counts if its constructor
8776/// chain or a property initializer can ([`class_construction_can_abort`]).
8777pub fn subroutine_can_abort(sub: &Symbol<'_>) -> bool {
8778    let mut visiting = std::collections::HashSet::new();
8779    can_abort_in(sub, &mut visiting)
8780}
8781
8782thread_local! {
8783    /// The uid of every virtual-method symbol (a base declaration, each
8784    /// override, an interface-class prototype) in a vtable slot at least one
8785    /// of whose implementations can abort. A call to a virtual method
8786    /// resolves statically to the BASE declaration, whose own body is empty,
8787    /// so [`can_abort_in`] cannot learn about an aborting override from the
8788    /// callee's body: codegen computes this set once, to a fixed point,
8789    /// before lowering any call site ([`mark_aborting_virtual`]).
8790    static ABORTING_VIRTUALS: std::cell::RefCell<HashSet<crate::symid::SymbolId>> =
8791        std::cell::RefCell::new(HashSet::new());
8792    /// Whether a `randomize()` call can end the process: some class's
8793    /// `pre_randomize()`/`post_randomize()` hook (which `randomize()` runs
8794    /// on the receiver or on any object it reaches, chosen by dynamic class)
8795    /// can abort ([`set_randomize_can_abort`]).
8796    static RANDOMIZE_CAN_ABORT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
8797}
8798
8799/// Empties the aborting-virtual set (and the `randomize()` flag); codegen
8800/// calls this before building a design's vtables.
8801pub fn reset_aborting_virtuals() {
8802    ABORTING_VIRTUALS.with(|s| s.borrow_mut().clear());
8803    RANDOMIZE_CAN_ABORT.with(|c| c.set(false));
8804}
8805
8806/// Declares that `randomize()` calls can abort (a `pre_randomize()` or
8807/// `post_randomize()` hook of the design can). Returns whether this changed the
8808/// flag, for a caller iterating to a fixed point.
8809pub fn set_randomize_can_abort() -> bool {
8810    RANDOMIZE_CAN_ABORT.with(|c| !c.replace(true))
8811}
8812
8813/// Whether [`set_randomize_can_abort`] has been called since the last reset.
8814pub fn randomize_can_abort() -> bool {
8815    RANDOMIZE_CAN_ABORT.with(|c| c.get())
8816}
8817
8818/// Records `uid` (a virtual method belonging to a slot with an aborting
8819/// implementation) as one whose calls can abort. Returns whether it was newly
8820/// recorded, so a caller iterating to a fixed point knows when it is done.
8821pub fn mark_aborting_virtual(uid: crate::symid::SymbolId) -> bool {
8822    ABORTING_VIRTUALS.with(|s| s.borrow_mut().insert(uid))
8823}
8824
8825fn can_abort_in(
8826    sub: &Symbol<'_>,
8827    visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8828) -> bool {
8829    if sub
8830        .subroutine_flags()
8831        .contains(sv_lang::MethodFlags::DPI_IMPORT)
8832    {
8833        return true;
8834    }
8835    if ABORTING_VIRTUALS.with(|s| s.borrow().contains(&sub.uid())) {
8836        return true;
8837    }
8838    if !visiting.insert(sub.uid()) {
8839        return false;
8840    }
8841    let Some(body) = proc_body(sub) else {
8842        return false;
8843    };
8844    if node_can_abort(body.as_sem_node(), Some(sub), visiting) {
8845        return true;
8846    }
8847    // A constructor's class-level work (property initializers, the
8848    // `extends Base(args)` call) runs with it, as part of the `new` that
8849    // invokes it.
8850    sub.subroutine_flags()
8851        .contains(sv_lang::MethodFlags::CONSTRUCTOR)
8852        && sub
8853            .parent()
8854            .and_then(|class| class.as_type())
8855            .is_some_and(|ty| class_level_can_abort(ty, visiting))
8856}
8857
8858/// Whether constructing an object of class `ty` (`new`: every constructor
8859/// from the root base class down, every level's property initializers and
8860/// `extends Base(args)` call) can end the process.
8861pub fn class_construction_can_abort(ty: sv_lang::Type<'_>) -> bool {
8862    let mut visiting = std::collections::HashSet::new();
8863    construction_can_abort(ty, &mut visiting)
8864}
8865
8866fn construction_can_abort(
8867    ty: sv_lang::Type<'_>,
8868    visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8869) -> bool {
8870    let mut level = Some(ty);
8871    while let Some(t) = level {
8872        if !t.is_class() {
8873            return false;
8874        }
8875        if class_level_can_abort(t, visiting) {
8876            return true;
8877        }
8878        if t.class_constructor()
8879            .is_some_and(|ctor| can_abort_in(&ctor, visiting))
8880        {
8881            return true;
8882        }
8883        level = t.class_base();
8884    }
8885    false
8886}
8887
8888/// Whether the work one class level does around its constructor body can
8889/// end the process: its property initializers and its `extends Base(args)`
8890/// call (the explicit-`super.new` form is part of the constructor body).
8891fn class_level_can_abort(
8892    ty: sv_lang::Type<'_>,
8893    visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8894) -> bool {
8895    let class_sym = ty.as_symbol();
8896    // `class A; A next = new; endclass` constructs itself: the guard keeps
8897    // the analysis finite (a class level is recorded once, under its own uid).
8898    if !visiting.insert(class_sym.uid()) {
8899        return false;
8900    }
8901    if let Some(call) = ty.class_base_constructor_call()
8902        && node_can_abort(call.as_sem_node(), None, visiting)
8903    {
8904        return true;
8905    }
8906    for member in class_sym.members() {
8907        if member.kind() == SymbolKind::ClassProperty
8908            && let Some(init) = member.initializer()
8909            && node_can_abort(init.as_sem_node(), None, visiting)
8910        {
8911            return true;
8912        }
8913    }
8914    false
8915}
8916
8917/// [`can_abort_in`]'s walk over one subtree. `enclosing` is the subroutine
8918/// the subtree belongs to (a `super.new(...)` finds its base class through
8919/// it).
8920fn node_can_abort<'d>(
8921    root: SemNode<'d>,
8922    enclosing: Option<&Symbol<'d>>,
8923    visiting: &mut std::collections::HashSet<crate::symid::SymbolId>,
8924) -> bool {
8925    let mut stack = vec![root];
8926    while let Some(n) = stack.pop() {
8927        // `$cast(dest, src);` as a task is a run-time error when it fails
8928        // (LRM 6.24.2), ending the process like `$fatal`.
8929        if let Some(s) = n.as_statement()
8930            && s.kind() == StatementKind::ExpressionStatement
8931            && let Some(e) = s.expr()
8932            && is_failable_task_cast(&e)
8933        {
8934            return true;
8935        }
8936        if let Some(e) = n.as_expression() {
8937            if crate::netlist::vif::is_member_access(&e)
8938                || crate::netlist::vif::call_targets(&e).is_some()
8939            {
8940                return true;
8941            }
8942            if e.kind() == ExpressionKind::Call {
8943                if e.is_system_call() == Some(true) {
8944                    if e.system_subroutine().is_some_and(|s| {
8945                        match s.known_name_id() {
8946                            KnownSystemName::Finish
8947                            | KnownSystemName::Stop
8948                            | KnownSystemName::Fatal => true,
8949                            // `obj.randomize()` runs the class's
8950                            // `pre_/post_randomize()` hooks (LRM 18.6.2).
8951                            KnownSystemName::Randomize => randomize_can_abort(),
8952                            _ => false,
8953                        }
8954                    }) {
8955                        return true;
8956                    }
8957                } else if let Some(callee) = e.call_subroutine() {
8958                    // `p.kill()` ends the caller when `p` is the caller (or
8959                    // one of its ancestors), which is only known at run time.
8960                    if std_process_method(&callee).is_some_and(|m| m == "kill") {
8961                        return true;
8962                    }
8963                    // An interface `extern` call runs its implementations.
8964                    for target in crate::netlist::itf::call_callees(&e) {
8965                        if can_abort_in(&target, visiting) {
8966                            return true;
8967                        }
8968                    }
8969                }
8970            } else if e.kind() == ExpressionKind::NewClass {
8971                if let Some(ctor_call) = e.new_class_constructor_call() {
8972                    stack.push(ctor_call.as_sem_node());
8973                }
8974                // `super.new(...)`'s expression type is `void`: the class
8975                // whose construction it continues is the enclosing
8976                // constructor's base. Any other `new` constructs its type.
8977                let constructed = if e.new_class_is_super_class() == Some(true) {
8978                    enclosing
8979                        .and_then(|sub| sub.parent())
8980                        .and_then(|class| class.as_type())
8981                        .and_then(|ty| ty.class_base())
8982                } else {
8983                    e.expr_type().map(|t| t.canonical())
8984                };
8985                if constructed.is_some_and(|ty| construction_can_abort(ty, visiting)) {
8986                    return true;
8987                }
8988            }
8989        }
8990        stack.extend(live_children(&n));
8991    }
8992    false
8993}
8994
8995/// Whether `e` is the task form of `$cast` whose destination type can reject
8996/// a value (a class handle or an enum; any other destination accepts every
8997/// source, so the call can never be the run-time error of LRM 6.24.2).
8998fn is_failable_task_cast(e: &Expression<'_>) -> bool {
8999    if e.kind() != ExpressionKind::Call
9000        || e.is_system_call() != Some(true)
9001        || !e
9002            .system_subroutine()
9003            .is_some_and(|s| s.known_name_id() == KnownSystemName::Cast)
9004    {
9005        return false;
9006    }
9007    let ops = e.operands();
9008    let Some(&dest) = ops.first() else {
9009        return false;
9010    };
9011    let dest = if dest.kind() == ExpressionKind::Assignment {
9012        dest.left().unwrap_or(dest)
9013    } else {
9014        dest
9015    };
9016    dest.expr_type()
9017        .is_some_and(|t| t.is_class() || t.is_enum())
9018}
9019
9020/// The target of an `output`/`inout` call argument: slang binds it as
9021/// `actual = <formal>`.
9022fn call_actual_lvalue<'d>(actual: Expression<'d>) -> Expression<'d> {
9023    if actual.kind() == ExpressionKind::Assignment {
9024        actual.left().unwrap_or(actual)
9025    } else {
9026        actual
9027    }
9028}
9029
9030/// The variables of the activation that forks `branch` that the branch
9031/// uses: automatic variables and inlined-task formals it does not declare
9032/// itself (static ones are the instance's signals).
9033fn captured_vars<'d>(branch: &ProcessCfg<'d>) -> Vec<Symbol<'d>> {
9034    let own: std::collections::HashSet<crate::symid::SymbolId> =
9035        super::liveness::declared_local_ids(branch);
9036    captured_in_excluding(vec![branch.body.as_sem_node()], &own)
9037}
9038
9039/// [`captured_vars`] over explicit AST roots, for a branch that declares nothing.
9040fn captured_in<'d>(roots: Vec<sv_lang::SemNode<'d>>) -> Vec<Symbol<'d>> {
9041    captured_in_excluding(roots, &std::collections::HashSet::new())
9042}
9043
9044fn captured_in_excluding<'d>(
9045    roots: Vec<sv_lang::SemNode<'d>>,
9046    own: &std::collections::HashSet<crate::symid::SymbolId>,
9047) -> Vec<Symbol<'d>> {
9048    let mut out: Vec<Symbol<'d>> = Vec::new();
9049    let mut seen = std::collections::HashSet::new();
9050    let mut stack = roots;
9051    while let Some(n) = stack.pop() {
9052        if let Some(e) = n.as_expression()
9053            && crate::symid::is_value_name(e.kind())
9054            && let Some(r) = e.referenced_symbol()
9055        {
9056            let procedural = (r.kind() == SymbolKind::Variable
9057                && r.variable_lifetime() == sv_lang::VariableLifetime::Automatic
9058                && r.parent().is_some_and(|p| {
9059                    matches!(
9060                        p.kind(),
9061                        SymbolKind::StatementBlock | SymbolKind::Subroutine
9062                    )
9063                }))
9064                || r.kind() == SymbolKind::FormalArgument;
9065            if procedural && !own.contains(&r.uid()) && seen.insert(r.uid()) {
9066                out.push(r);
9067            }
9068        }
9069        stack.extend(live_children(&n));
9070    }
9071    out
9072}
9073
9074/// The automatic variables the block statement `stmt` declares: created and
9075/// initialized each time the block is entered (LRM 1800-2023 §6.21), in
9076/// declaration order. A `for (int i = ...)` loop's variables live in the
9077/// implicit block around it, entered as the loop starts; with
9078/// `with_loop_vars` false they are left out (for a lowering that sets them
9079/// as part of the loop).
9080pub fn block_automatic_vars<'d>(stmt: &Statement<'d>, with_loop_vars: bool) -> Vec<Symbol<'d>> {
9081    if stmt.kind() != StatementKind::Block {
9082        return Vec::new();
9083    }
9084    let Some(block) = stmt.block_symbol() else {
9085        return Vec::new();
9086    };
9087    let mut loop_vars = std::collections::HashSet::new();
9088    if !with_loop_vars {
9089        let mut stack = stmt.statements();
9090        while let Some(s) = stack.pop() {
9091            if s.kind() == StatementKind::List {
9092                stack.extend(s.statements());
9093            } else if s.kind() == StatementKind::ForLoop {
9094                loop_vars.extend(s.for_loop_vars().iter().map(|v| v.uid()));
9095            }
9096        }
9097    }
9098    block
9099        .members()
9100        .filter(|m| {
9101            m.kind() == SymbolKind::Variable
9102                && m.variable_lifetime() == sv_lang::VariableLifetime::Automatic
9103                && !loop_vars.contains(&m.uid())
9104        })
9105        .collect()
9106}
9107
9108/// The prefix of the names vj gives the blocks it introduces into a
9109/// design (`randsequence`'s expansion): they are not part of any
9110/// hierarchical name the design prints.
9111pub const INTERNAL_NAME_PREFIX: &str = "__vj_";
9112
9113/// `sym`'s hierarchical name as the design sees it (`%m`, a message's
9114/// scope): the blocks vj introduced ([`INTERNAL_NAME_PREFIX`]) left out.
9115pub fn shown_path(sym: &Symbol<'_>) -> String {
9116    let path = sym.hierarchical_path();
9117    if !path.contains(INTERNAL_NAME_PREFIX) {
9118        return path;
9119    }
9120    let mut out = String::with_capacity(path.len());
9121    let mut rest = path.as_str();
9122    while let Some(i) = rest.find(INTERNAL_NAME_PREFIX) {
9123        let at_segment = i == 0 || rest.as_bytes()[i - 1] == b'.';
9124        if !at_segment {
9125            out.push_str(&rest[..i + INTERNAL_NAME_PREFIX.len()]);
9126            rest = &rest[i + INTERNAL_NAME_PREFIX.len()..];
9127            continue;
9128        }
9129        // Drop the segment and the `.` before it (or after it, at the start).
9130        let end = rest[i..].find('.').map_or(rest.len(), |e| i + e);
9131        if i > 0 {
9132            out.push_str(&rest[..i - 1]);
9133            rest = &rest[end..];
9134        } else {
9135            rest = rest[end..].strip_prefix('.').unwrap_or(&rest[end..]);
9136        }
9137    }
9138    out.push_str(rest);
9139    out
9140}
9141
9142/// Whether a variable of type `ty` can hold a class handle or a queue /
9143/// dynamic / associative array handle (directly or in a struct field): the
9144/// variables whose block exit owes a release. A fixed-size array of handles
9145/// is not covered (its elements are released on overwrite only).
9146pub fn may_hold_handles(ty: &sv_lang::Type<'_>) -> bool {
9147    if ty.is_class() || crate::state::storage::is_collection(ty) {
9148        return true;
9149    }
9150    ty.is_unpacked_struct()
9151        && ty
9152            .fields()
9153            .any(|f| f.value_type().is_some_and(|t| may_hold_handles(&t)))
9154}
9155
9156/// Whether `var` is an automatic variable a block declares (initialized on
9157/// entry to that block, [`block_automatic_vars`]).
9158pub fn is_automatic_block_var(var: &Symbol<'_>) -> bool {
9159    var.variable_lifetime() == sv_lang::VariableLifetime::Automatic
9160        && var
9161            .parent()
9162            .is_some_and(|p| p.kind() == SymbolKind::StatementBlock)
9163}
9164
9165/// A task's own variables: members of the task scope (not its formals or
9166/// result variable) and of every block inside its body.
9167fn task_locals<'d>(task: &Symbol<'d>) -> Vec<Symbol<'d>> {
9168    let formal_ids: std::collections::HashSet<crate::symid::SymbolId> =
9169        task.subroutine_arguments().map(|a| a.uid()).collect();
9170    let ret_id = task.subroutine_return_val_var().map(|r| r.uid());
9171    let mut out = Vec::new();
9172    let mut seen = std::collections::HashSet::new();
9173    let mut take = |m: Symbol<'d>, out: &mut Vec<Symbol<'d>>| {
9174        let id = m.uid();
9175        if m.kind() == SymbolKind::Variable
9176            && !formal_ids.contains(&id)
9177            && Some(&id) != ret_id.as_ref()
9178            && seen.insert(id)
9179        {
9180            out.push(m);
9181        }
9182    };
9183    for m in task.members() {
9184        take(m, &mut out);
9185    }
9186    if let Some(body) = proc_body(task) {
9187        let mut stack = vec![body.as_sem_node()];
9188        while let Some(n) = stack.pop() {
9189            if let Some(s) = n.as_statement() {
9190                for block in block_scopes(&s) {
9191                    for m in block.members() {
9192                        take(m, &mut out);
9193                    }
9194                }
9195            }
9196            let mut kids = live_children(&n);
9197            kids.reverse();
9198            stack.extend(kids);
9199        }
9200    }
9201    out
9202}
9203
9204// --- Segment IR (PR-041, PR-042, PR-043; SegId numbering CG-098, CG-099) ---
9205
9206/// Task S3b's real liveness/frame-layout shape now lives in
9207/// `process::liveness` (`FrameLayout`/`FrameSlotDesc`/`FrameSlotSource`) --
9208/// re-exported here under this task's original name so every existing call
9209/// site (this file's own `lower_to_segments`, `tests/process_cfg.rs`) keeps
9210/// working unchanged.
9211pub use super::liveness::FrameLayout;
9212use super::liveness::FrameSlotSource;
9213
9214/// The codegen-facing, already-liveness-annotated lowering shape (PR-042).
9215/// `live_in`/`live_out` are `FrameLayout::slots` indices (Task S3b: see
9216/// `lower_to_segments`'s doc comment for exactly which slots those are).
9217#[derive(Debug, Clone)]
9218pub struct Segment<'d> {
9219    pub id: SegId,
9220    pub live_in: Vec<u32>,
9221    /// Mirrors [`BasicBlock::bindings`] 1:1 -- run before `body`.
9222    pub bindings: Vec<ArgBinding<'d>>,
9223    /// Mirrors [`BasicBlock::this_var`] 1:1.
9224    pub this_var: Option<Symbol<'d>>,
9225    /// Mirrors [`BasicBlock::ref_aliases`] 1:1.
9226    pub ref_aliases: Vec<(Symbol<'d>, Symbol<'d>)>,
9227    pub body: Vec<Statement<'d>>,
9228    /// Mirrors [`BasicBlock::probes`] 1:1 -- indices are into `body`.
9229    pub probes: Vec<(usize, Statement<'d>)>,
9230    /// Mirrors [`BasicBlock::disable_probes`] 1:1 -- indices are into `body`.
9231    pub disable_probes: Vec<(usize, Statement<'d>)>,
9232    /// Mirrors [`BasicBlock::frame_probes`] 1:1 -- indices are into `body`.
9233    pub frame_probes: Vec<(usize, FrameProbe<'d>)>,
9234    /// Mirrors [`BasicBlock::block_events`] 1:1 -- indices are into `body`.
9235    pub block_events: Vec<(usize, Symbol<'d>, bool)>,
9236    /// Mirrors [`BasicBlock::stmt_scopes`] 1:1 -- indices are into `body`.
9237    pub stmt_scopes: Vec<(usize, Symbol<'d>)>,
9238    /// Mirrors [`BasicBlock::step_exprs`] 1:1 -- see its doc comment.
9239    pub step_exprs: Vec<Expression<'d>>,
9240    /// Mirrors [`BasicBlock::repeat_init`] 1:1 -- see its doc comment.
9241    pub repeat_init: Option<(RepeatCounterId, RepeatCount<'d>)>,
9242    /// Mirrors [`BasicBlock::repeat_decrement`] 1:1 -- see its doc comment.
9243    pub repeat_decrement: Option<RepeatCounterId>,
9244    /// Mirrors [`BasicBlock::expect_resume`] 1:1 -- see its doc comment.
9245    pub expect_resume: bool,
9246    pub terminator: SegTerminator<'d>,
9247}
9248
9249/// PR-043: covers every [`Terminator`] variant with no lossy fallback (see
9250/// [`lower_to_segments`]'s 1:1 mapping).
9251#[derive(Debug, Clone)]
9252pub enum SegTerminator<'d> {
9253    Goto(SegId),
9254    CondGoto {
9255        cond: Expression<'d>,
9256        t: SegId,
9257        f: SegId,
9258    },
9259    Suspend {
9260        point: SuspendPoint<'d>,
9261        resume: SegId,
9262        live_out: Vec<u32>,
9263    },
9264    End {
9265        loop_back: Option<SegId>,
9266    },
9267    /// The lowering of [`Terminator::Switch`].
9268    SwitchGoto {
9269        on: SwitchOn<'d>,
9270        arms: Vec<SegId>,
9271        default: SegId,
9272        report: Option<sv_lang::SourceLoc>,
9273    },
9274    /// R-run Task 8: the lossless lowering of [`Terminator::RepeatCheck`] --
9275    /// see that variant's own doc comment for why a `repeat(N)` loop's
9276    /// per-iteration decision cannot be represented as an ordinary
9277    /// `CondGoto` (no `cond: Expression` exists for "compiler-synthesized
9278    /// counter != 0"). **Deviation from this file's own module doc comment**
9279    /// (Task 1's "frozen, codegen-exhausted `SegTerminator`" policy): that
9280    /// policy was written to rule out `ForkJoin` specifically (which has no
9281    /// honest `SegTerminator` shape at all, ever, this wave); a suspend
9282    /// nested inside a `repeat` body is a genuinely new kind of branch
9283    /// condition this crate had no way to construct before this task, not a
9284    /// reachability gap in the pre-existing four variants -- recorded in
9285    /// this task's own deviations.
9286    CondGotoCounter {
9287        counter: RepeatCounterId,
9288        t: SegId,
9289        f: SegId,
9290    },
9291    /// R-run Task 17 (PR-047/PR-061): the lossless lowering of
9292    /// [`Terminator::ForkJoin`] -- a real, new `SegTerminator` shape, added
9293    /// for the identical reason `CondGotoCounter` above already documents a
9294    /// deviation from this file's own "frozen `SegTerminator`" policy note:
9295    /// that note was written to rule out `ForkJoin` specifically because
9296    /// NOTHING had implemented fork/join scheduling yet; this task is that
9297    /// implementation for `StatementBlockKind::JoinAll` (codegen enforces
9298    /// the JoinAll-only scope with a named `LoweringError` for `JoinAny`/
9299    /// `JoinNone` -- this type carries `kind` through losslessly so codegen
9300    /// has it to check, rather than this crate silently narrowing what it
9301    /// represents). `branches` is exactly [`Terminator::ForkJoin`]'s own
9302    /// field, carried through unchanged (each branch is still a full,
9303    /// independently suspend-capable nested [`ProcessCfg`] -- codegen
9304    /// recursively lowers each one into its own real compiled kernel).
9305    /// `join_blk`/`live_out` mirror `Suspend`'s own `resume`/`live_out`
9306    /// fields exactly: a fork/join site suspends the calling process (until
9307    /// every spawned child completes, for `JoinAll`) just as surely as a
9308    /// `Suspend` terminator does, so the same permanent-frame-slot
9309    /// liveness/writeback discipline applies -- see [`lower_to_segments`]'s
9310    /// own fill rule.
9311    ForkJoin {
9312        kind: StatementBlockKind,
9313        branches: Vec<ProcessCfg<'d>>,
9314        join_blk: SegId,
9315        live_out: Vec<u32>,
9316    },
9317    /// The lowering of [`Terminator::Assertion`].
9318    AssertGoto {
9319        stmt: Statement<'d>,
9320        pass: SegId,
9321        fail: SegId,
9322        done: SegId,
9323    },
9324    /// The lowering of [`Terminator::Case`]: goes to `arms[i]` for the first
9325    /// item of `stmt` whose labels match its selector, else `default`.
9326    CaseGoto {
9327        stmt: Statement<'d>,
9328        arms: Vec<SegId>,
9329        default: SegId,
9330    },
9331}
9332
9333/// PR-041/042/043: a 1:1, source-order-stable (CG-098, CG-099) lowering of
9334/// each `ProcessCfg` [`BasicBlock`] into a [`Segment`] -- `blocks[i]` always
9335/// becomes `SegId(i)`, so segment numbering is fixed the moment `build_cfg`
9336/// allocates its blocks, well before this function (or any LLVM `Context`)
9337/// ever runs.
9338///
9339/// **Task S3b's `live_in`/`live_out` fill rule:** every `FrameLayout` slot
9340/// with source `FrameSlotSource::DeclaredVar` is a *permanent* slot
9341/// (PR-017/018 -- valid for the process's entire lifetime, never
9342/// added/removed by liveness), so its index belongs in `live_in`/`live_out`
9343/// at every segment, unconditionally. A `FrameSlotSource::Temporary` slot's
9344/// index would additionally need to appear only where
9345/// `liveness::compute_liveness`'s own backward fixpoint says it is actually
9346/// live; this task's own restricted CFG-level IR never has a positive source
9347/// for a `Temporary` slot at all (one is only ever minted once CODEGEN's
9348/// Task S3c starts decomposing expressions into named SSA values with no
9349/// corresponding `sv_lang` symbol), so `frame.slots` here is, for every
9350/// fixture this task can construct, exactly the `DeclaredVar` set -- see
9351/// `liveness.rs`'s own module doc comment for the full reasoning. A future
9352/// Task S3c revision of this function additionally intersects
9353/// `compute_liveness`'s per-segment result with whatever `Temporary` slots
9354/// exist by then; this rule does not change for `DeclaredVar` slots.
9355pub fn lower_to_segments<'d>(cfg: &ProcessCfg<'d>, frame: &FrameLayout) -> Vec<Segment<'d>> {
9356    // R-run Task 8: a `repeat` loop's synthetic counter is ALSO permanent --
9357    // it must survive across a suspend/resume call boundary exactly like a
9358    // declared local (see `FrameSlotSource::SyntheticCounter`'s own doc
9359    // comment), so its slot index belongs in `live_in`/`live_out` at every
9360    // segment unconditionally too, same rule as `DeclaredVar`.
9361    let permanent: Vec<u32> = frame
9362        .slots
9363        .iter()
9364        .enumerate()
9365        .filter(|(_, s)| {
9366            matches!(
9367                s.source,
9368                FrameSlotSource::DeclaredVar(_)
9369                    | FrameSlotSource::SyntheticCounter(_)
9370                    | FrameSlotSource::Temporary(_)
9371            )
9372        })
9373        .map(|(i, _)| i as u32)
9374        .collect();
9375
9376    cfg.blocks
9377        .iter()
9378        .enumerate()
9379        .map(|(i, b)| Segment {
9380            id: SegId(i as u32),
9381            live_in: permanent.clone(),
9382            bindings: b.bindings.clone(),
9383            this_var: b.this_var,
9384            ref_aliases: b.ref_aliases.clone(),
9385            body: b.stmts.clone(),
9386            probes: b.probes.clone(),
9387            disable_probes: b.disable_probes.clone(),
9388            frame_probes: b.frame_probes.clone(),
9389            block_events: b.block_events.clone(),
9390            stmt_scopes: b.stmt_scopes.clone(),
9391            step_exprs: b.step_exprs.clone(),
9392            repeat_init: b.repeat_init.clone(),
9393            repeat_decrement: b.repeat_decrement,
9394            expect_resume: b.expect_resume,
9395            terminator: lower_terminator(&b.terminator, &permanent),
9396        })
9397        .collect()
9398}
9399
9400/// Task 1: `true` iff any block in `cfg` closes with `Terminator::ForkJoin`
9401/// -- fork/join has no runtime scheduling semantics this wave (`docs/
9402/// design/process.md` §7: "construction only, never scheduled"), so
9403/// [`lower_to_segments`]/`lower_terminator` cannot honestly produce a
9404/// `SegTerminator` for one (there is no shape in that frozen, codegen-
9405/// exhausted enum for "spawn N branches" -- see this file's module doc
9406/// comment's `SegTerminator`-freeze note). Every real caller (this crate's
9407/// own `vj::run`) must check this and fail loudly *before* calling
9408/// `lower_to_segments` on a CFG that might contain one; `lower_terminator`
9409/// itself still documents and asserts the precondition (`unreachable!`) as
9410/// a caller-discipline internal invariant, exactly like `finish()`'s own
9411/// "every block must be closed" `.expect()` -- not a condition reachable
9412/// from malformed SV source once a caller has checked this predicate first.
9413pub fn contains_unschedulable_fork_join(cfg: &ProcessCfg<'_>) -> bool {
9414    cfg.blocks
9415        .iter()
9416        .any(|b| matches!(b.terminator, Terminator::ForkJoin { .. }))
9417}
9418
9419/// R-run Task 17 (PR-047/PR-061): `true` iff any block in `cfg` closes with
9420/// a `Terminator::ForkJoin` whose `kind` is `JoinAny`/`JoinNone` -- the two
9421/// fork/join shapes this task's own scope note ("Scope for this task:
9422/// `JoinAll` only") leaves unimplemented at the codegen layer.
9423/// **Deliberately narrower than [`contains_unschedulable_fork_join`]
9424/// above, which this function does NOT replace or modify** (that
9425/// function's existing callers -- `vj::aot`, which this task does
9426/// not extend to support ANY fork/join shape -- keep their original,
9427/// unchanged "reject every fork/join" behavior unaffected): a `JoinAll`
9428/// fork/join now has a real [`SegTerminator::ForkJoin`] shape
9429/// [`lower_to_segments`] can produce, so a caller that DOES support it
9430/// (`vj::run`) needs a predicate that only rejects the two kinds
9431/// still genuinely unsupported, letting `JoinAll` flow through to
9432/// `lower_to_segments`/codegen instead of being rejected pre-emptively.
9433pub fn contains_unsupported_fork_join_kind(cfg: &ProcessCfg<'_>) -> bool {
9434    cfg.blocks.iter().any(|b| {
9435        matches!(
9436            b.terminator,
9437            Terminator::ForkJoin {
9438                kind: StatementBlockKind::JoinAny | StatementBlockKind::JoinNone,
9439                ..
9440            }
9441        )
9442    })
9443}
9444
9445fn lower_terminator<'d>(t: &Terminator<'d>, permanent: &[u32]) -> SegTerminator<'d> {
9446    match t {
9447        Terminator::Fallthrough(b) => SegTerminator::Goto(SegId(b.0)),
9448        Terminator::Switch {
9449            on,
9450            arms,
9451            default_blk,
9452            report,
9453        } => SegTerminator::SwitchGoto {
9454            on: on.clone(),
9455            arms: arms.iter().map(|b| SegId(b.0)).collect(),
9456            default: SegId(default_blk.0),
9457            report: *report,
9458        },
9459        Terminator::Branch {
9460            cond,
9461            then_blk,
9462            else_blk,
9463        } => SegTerminator::CondGoto {
9464            cond: *cond,
9465            t: SegId(then_blk.0),
9466            f: SegId(else_blk.0),
9467        },
9468        Terminator::Suspend { point, resume_blk } => SegTerminator::Suspend {
9469            point: point.clone(),
9470            resume: SegId(resume_blk.0),
9471            live_out: permanent.to_vec(),
9472        },
9473        Terminator::End { loop_back } => SegTerminator::End {
9474            loop_back: loop_back.map(|b| SegId(b.0)),
9475        },
9476        // Task 1: `Loop` lowers losslessly to `SegTerminator::Goto` -- see
9477        // the module doc comment's `SegTerminator`-freeze note. Its real
9478        // per-iteration decision already lives on whichever block `entry`
9479        // names (a real `Branch`, or `Fallthrough` for `forever`), so
9480        // nothing this enum could represent is lost.
9481        Terminator::Loop { entry, .. } => SegTerminator::Goto(SegId(entry.0)),
9482        // R-run Task 8: see `SegTerminator::CondGotoCounter`'s own doc
9483        // comment for why this is a real, new lowering (not a lossless
9484        // `Goto`/`CondGoto` reuse the way `Loop` above is).
9485        Terminator::RepeatCheck {
9486            counter,
9487            then_blk,
9488            else_blk,
9489        } => SegTerminator::CondGotoCounter {
9490            counter: *counter,
9491            t: SegId(then_blk.0),
9492            f: SegId(else_blk.0),
9493        },
9494        // R-run Task 17: no longer unreachable -- see `SegTerminator::
9495        // ForkJoin`'s own doc comment. Lossless: `branches`/`kind` carry
9496        // straight through, `join_blk`/`live_out` get the exact same
9497        // treatment `Suspend`'s `resume`/`live_out` get above (a fork/join
9498        // site is a real suspend point too).
9499        Terminator::ForkJoin {
9500            kind,
9501            branches,
9502            join_blk,
9503        } => SegTerminator::ForkJoin {
9504            kind: *kind,
9505            branches: branches.clone(),
9506            join_blk: SegId(join_blk.0),
9507            live_out: permanent.to_vec(),
9508        },
9509        Terminator::Assertion {
9510            stmt,
9511            pass_blk,
9512            fail_blk,
9513            done_blk,
9514        } => SegTerminator::AssertGoto {
9515            stmt: *stmt,
9516            pass: SegId(pass_blk.0),
9517            fail: SegId(fail_blk.0),
9518            done: SegId(done_blk.0),
9519        },
9520        Terminator::Case {
9521            stmt,
9522            arms,
9523            default_blk,
9524        } => SegTerminator::CaseGoto {
9525            stmt: *stmt,
9526            arms: arms.iter().map(|b| SegId(b.0)).collect(),
9527            default: SegId(default_blk.0),
9528        },
9529    }
9530}
9531
9532// --- PR-074: cross-check against AnalyzedProcedure::timing_controls() ---
9533
9534/// Counts every `Timed` statement anywhere in `body`'s subtree, independent
9535/// of whether `build_cfg` would actually accept each one (a rejected
9536/// `TimingControlKind` is still a `Timed` statement) -- this subsystem's own
9537/// "how many suspend points did the tree walk find" half of PR-074's
9538/// cross-check.
9539pub fn count_timed_statements(body: &Statement<'_>) -> usize {
9540    let mut count = 0usize;
9541    let mut stack = vec![body.as_sem_node()];
9542    while let Some(n) = stack.pop() {
9543        if let Some(s) = n.as_statement()
9544            && s.kind() == StatementKind::Timed
9545        {
9546            count += 1;
9547        }
9548        stack.extend(live_children(&n));
9549    }
9550    count
9551}
9552
9553/// PR-074: "the subsystem's own suspend-point tree walk must be cross-checked
9554/// against `AnalyzedProcedure::timing_controls()`; a mismatch ... MUST be a
9555/// hard-fail bug, not a warning." Factored as a pure comparator over two
9556/// counts (rather than requiring every caller to thread a live
9557/// `sv_lang::Analysis` through) so a test can fault-inject a mismatch
9558/// directly -- see `cross_check_hard_fails_on_a_fault_injected_mismatch` in
9559/// `tests/process_cfg.rs`. The real call site passes
9560/// `own_walk_count = count_timed_statements(body)` and
9561/// `analyzed_count = analyzed_procedure.timing_controls().count()`.
9562pub fn check_suspend_count_matches(
9563    own_walk_count: usize,
9564    analyzed_count: usize,
9565) -> Result<(), LoweringError> {
9566    if own_walk_count == analyzed_count {
9567        Ok(())
9568    } else {
9569        Err(LoweringError::malformed(format!(
9570            "PR-074 suspend-point cross-check failed: this subsystem's own tree walk found \
9571             {own_walk_count} Timed statement(s), but AnalyzedProcedure::timing_controls() \
9572             found {analyzed_count} -- this is a hard bug (a missed or double-counted \
9573             suspend point), never a warning"
9574        )))
9575    }
9576}