Skip to main content

sv_sim_sim/netlist/
template.rs

1//! Task 4 (NL-028, NL-121): the full `Template` shape (netlist-analysis.md
2//! KEY DATA STRUCTURES), plus `SubInstanceSlot` — the deduped body's own
3//! enumeration of its child instantiations, a Template-level structure
4//! DISTINCT from (but consistent in count/order with) the instance-level
5//! `InstanceTree`/`InstanceNode` hierarchy Task 1/3 already build.
6//!
7//! `signals`/`memories`/`ports` (Task 5) and `processes`/
8//! `continuous_assigns` (Task 7) are populated by later tasks; this task
9//! only establishes the field shape (empty `Vec`s) so `Template`'s public
10//! surface is stable from here on — no later task adds/removes a field.
11//!
12//! Task 5 (NL-029, NL-030, NL-120, NL-122): `SignalDecl`/`ValueDomain`/
13//! `MemoryDecl`/`PortDecl` gain their real fields, and [`build_members`]
14//! (called from `elaborate.rs`'s dedup pass, once per `Template`'s canonical
15//! body) populates `signals`/`memories`/`ports` for real.
16
17use crate::symid::SymbolUid;
18use std::collections::HashMap;
19
20use crate::netlist::{ElabError, SymbolKey, TemplateId, TemplateKey};
21
22/// (NL-030) Whether a signal's bit width fits STATE's 64-bit "narrow" value
23/// representation (ARCHITECTURE §6.2) or must spill to a wide, multi-word
24/// one. The boundary is inclusive: exactly 64 bits is `Narrow`. A declared
25/// memory array is never classified this way at all (NL-120) — `Memory`
26/// exists on this enum only because netlist-analysis.md's own `ValueDomain`
27/// shape includes it; this wave's `MemoryDecl` (below) has no `value_domain`
28/// field of its own to hold it, so this variant is declared but never
29/// constructed this wave (a later wave's per-memory-access IR is the natural
30/// place for it).
31#[derive(Clone, Copy, PartialEq, Eq, Debug)]
32pub enum ValueDomain {
33    /// `bit_width <= 64`.
34    Narrow { bits: u32 },
35    /// `bit_width > 64`.
36    Wide { bits: u32 },
37    /// A declared memory array (NL-120) — never folded into `Narrow`/`Wide`.
38    Memory,
39}
40
41/// (NL-029) Which of the three declaration forms a `SignalDecl` lowers.
42/// `Net` carries the net's own `sv_lang::NetKind` (`wire`/`wand`/…) — see
43/// `net_kind_of`'s doc comment (below) for a load-bearing deviation on how
44/// this is resolved from the pinned `sv-lang = "=0.1.9"` API.
45#[derive(Clone, Copy, PartialEq, Eq, Debug)]
46pub enum SignalKind {
47    Net(sv_lang::NetKind),
48    Variable,
49    Genvar,
50}
51
52/// (Revision, resolving a blocking review finding on NL-063 — narrower than
53/// netlist-analysis.md's own richer `DriverEntry`, which additionally carries
54/// `bit_range`/`strength`/`flags` for STATE's arena-conflict resolution, out
55/// of this wave's 37 ledger IDs.) Only `driver_kind` (the row's literal
56/// acceptance text) and `containing` (which process/continuous-assign owns
57/// this driver) are populated, by Task 8's `populate_driver_kinds` — this
58/// task (Task 5) only declares the type so `SignalDecl.driver_set`'s field
59/// type compiles, and leaves every `driver_set` it builds empty (`Vec::new()`
60/// — `sv_lang::Analysis`, the source of per-net driver data, does not exist
61/// until `analysis::analyze` runs, strictly after `elaborate()` returns; see
62/// the plan's Architecture section's load-bearing call-order note).
63#[derive(Clone, Copy, PartialEq, Eq, Debug)]
64pub struct DriverEntry {
65    pub driver_kind: sv_lang::DriverKind,
66    pub containing: SymbolKey,
67}
68
69/// A declared net/variable/genvar (NL-029). `key` is this member's
70/// `SymbolKey` within the owning `Template` — the SAME dense index
71/// `dedup::confirm_and_assign_symbol_keys` assigns from the identical
72/// `canonical.members()` enumeration order, so `signals[i].key` is never a
73/// second, independently-derived numbering.
74#[derive(Clone, Debug, PartialEq)]
75pub struct SignalDecl {
76    pub key: SymbolKey,
77    pub name: String,
78    pub kind: SignalKind,
79    pub value_domain: ValueDomain,
80    pub driver_set: Vec<DriverEntry>,
81}
82
83/// A declared unpacked memory array (NL-120) — `element_bits` from the
84/// array's element type's `Type::bit_width()`, `depth` from the unpacked
85/// dimension's `Type::fixed_range()` span
86/// (`(left - right).unsigned_abs() + 1`).
87#[derive(Clone, Debug, PartialEq, Eq)]
88pub struct MemoryDecl {
89    pub key: SymbolKey,
90    pub name: String,
91    pub element_bits: u32,
92    pub depth: u32,
93}
94
95/// (NL-037, NL-038 — Task 7 populates the `None` case only; Task 8 is where
96/// `always_comb`/`always_latch` get a real `Implicit` and `always_ff`/plain
97/// `always @(...)` get a real `Explicit`.) A process's static wake
98/// condition.
99#[derive(Clone, Debug, Default, PartialEq, Eq)]
100pub enum StaticSensitivity {
101    /// `always_comb`/`always_latch` (Task 8): the procedure's own read-set,
102    /// via the same `ReadSetLattice`/`run_dataflow` machinery `assign`
103    /// statements use (Decision 1).
104    Implicit { reads: Vec<SymbolKey> },
105    /// `always_ff`/plain `always @(posedge/negedge ...)` (Task 8): the
106    /// explicit edge-sensitivity list.
107    Explicit {
108        edges: Vec<(SymbolKey, crate::state::EdgeKind)>,
109    },
110    /// `initial`/`final` (Task 7, NL-037): no sensitivity at all — these
111    /// run once, at time zero / end-of-simulation, never re-triggered by a
112    /// signal change. Also the placeholder Task 7 leaves on
113    /// `always`/`always_comb`/`always_latch`/`always_ff` process kinds
114    /// until Task 8 fills in their real `Implicit`/`Explicit` derivation
115    /// (documented per call site — see `build_processes`).
116    #[default]
117    None,
118}
119
120/// (NL-037, NL-038) One elaborated `initial`/`final`/`always*` procedural
121/// block. `key` is this process's own `SymbolKey` within the owning
122/// `Template` — the same dense, position-based numbering every other member
123/// kind uses (`build_processes`'s own `scope.members().enumerate()` walk,
124/// the identical enumeration `build_members`/`confirm_and_assign_symbol_keys`
125/// already walk over the same canonical body).
126#[derive(Clone, Debug, PartialEq, Eq)]
127pub struct ProcessTemplate {
128    pub key: SymbolKey,
129    pub kind: sv_lang::ProceduralBlockKind,
130    pub sensitivity: StaticSensitivity,
131}
132
133/// (NL-034, NL-035) A continuous assignment — either a real `assign`
134/// statement in this `Template`'s own scope (`is_output_port_connection ==
135/// false`, `target`/`read_set` both resolved within this SAME scope), or a
136/// synthetic entry modeling one instance port connection at the PARENT
137/// scope's `Template` (LRM: "an instance port connection is, semantically,
138/// a continuous assignment" — netlist-analysis.md Decision/§A.5): `target`
139/// is then the CHILD port's own `SymbolKey` (within the CHILD `Template`'s
140/// numbering — the same key `SubInstanceSlot::port_bindings`'s first tuple
141/// element uses for the identical port), `read_set` is the connection's
142/// bound expression's read-set resolved at the PARENT scope
143/// (`exprwalk::expression_read_set`), and `is_output_port_connection`
144/// mirrors the child port's own `PortDecl.direction` (`true` for `Output`,
145/// `false` for `Input`/`Inout` — the field this task adds specifically so a
146/// consumer can tell "a write INTO the child port" (input) apart from "a
147/// write driven BY the child port" (output) without re-deriving direction a
148/// second, independently-reasoned way).
149#[derive(Clone, Debug, PartialEq, Eq)]
150pub struct ContAssignTemplate {
151    pub target: SymbolKey,
152    pub read_set: Vec<SymbolKey>,
153    pub is_output_port_connection: bool,
154}
155
156/// (NL-122) A module/program/interface port's own *declaration* — direction,
157/// resolved width, declaration order — populated purely from the module's
158/// own header, independent of whether/how any particular instantiation site
159/// connects it (distinct from port *connection* resolution, NL-035, which is
160/// Task 7's job).
161#[derive(Clone, Debug, PartialEq, Eq)]
162pub struct PortDecl {
163    /// (Task 7 addition, documented deviation from Task 5's original
164    /// shape — needed once port connections become synthetic
165    /// `ContAssignTemplate`/`SubInstanceSlot::port_bindings` entries
166    /// addressed BY this port's own `SymbolKey`, ledger NL-035/NL-121: a
167    /// `Port` member occupies a real position in its owning scope's
168    /// `members()` enumeration exactly like a `SignalDecl`/`MemoryDecl`
169    /// does, but nothing before this task ever recorded it.) Same
170    /// position-based numbering `build_members`'s own `scope.members().
171    /// enumerate()` walk assigns everything else.
172    pub key: SymbolKey,
173    pub name: String,
174    pub direction: PortDirection,
175    pub bits: u32,
176}
177
178/// The directions a module-level port can declare
179/// (`sv_lang::ArgumentDirection::{In,Out,InOut,Ref}`). A `Ref` port (LRM
180/// 1800-2023 23.2.2.3) is not a connection at all: the port variable IS the
181/// variable it is connected to (23.3.3.2, "references to the port variable
182/// shall be treated as hierarchical references to the variable to which it is
183/// connected"), so it has no driver and no continuous assignment.
184#[derive(Clone, Copy, PartialEq, Eq, Debug)]
185pub enum PortDirection {
186    Input,
187    Output,
188    Inout,
189    Ref,
190}
191
192/// A deduped module/program body's full compile-time inventory
193/// (netlist-analysis.md's `Template`). `signals` is in source declaration
194/// order (NL-028's own acceptance text); the other Vecs are populated by
195/// later tasks in this wave, per the field docs above.
196#[derive(Debug, Clone)]
197pub struct Template {
198    pub key: TemplateKey,
199    pub definition_name: String,
200    pub signals: Vec<SignalDecl>,
201    pub memories: Vec<MemoryDecl>,
202    pub processes: Vec<ProcessTemplate>,
203    pub continuous_assigns: Vec<ContAssignTemplate>,
204    pub sub_instances: Vec<SubInstanceSlot>,
205    pub ports: Vec<PortDecl>,
206}
207
208/// Template-level child-instantiation record (NL-121) — DISTINCT from
209/// `InstanceTree`/`InstanceNode` (instance-level, per-`InstanceId`,
210/// NL-011/NL-023): this is the deduped body's OWN enumeration of its child
211/// instantiations, letting CODEGEN/analysis walk the deduped `Template`
212/// graph without re-deriving it from `InstanceTree` every time.
213#[derive(Debug, Clone)]
214pub struct SubInstanceSlot {
215    /// One entry per port of the child template, in the child's own `ports`
216    /// declaration order: `(child port's SymbolKey, read_set)`. `read_set`
217    /// is the driving connection expression's read-set at the PARENT scope,
218    /// empty for an unconnected port — the same `Vec<SymbolKey>`
219    /// representation Task 7's synthetic `ContAssignTemplate::read_set`
220    /// uses for the identical underlying `expression_read_set` computation.
221    /// Left `Vec::new()` per slot by THIS task (structure-only — see this
222    /// task's own test); Task 7 is where `expression_read_set` first exists
223    /// and populates the real content.
224    pub port_bindings: Vec<(SymbolKey, Vec<SymbolKey>)>,
225    pub child_template: TemplateId,
226}
227
228/// Task 5 (NL-029, NL-030, NL-120, NL-122): walks `scope.members()` — the
229/// SAME flat, declaration-order iteration `dedup::confirm_and_assign_symbol_keys`
230/// already walks to assign `SymbolKey`s (`elaborate.rs`'s `dedup_templates`
231/// calls both over the same canonical body, so `signals[i].key`/
232/// `memories[i].key` line up with that same numbering by construction, never
233/// a second, independently-derived index) — and classifies every member into
234/// `Template.signals`/`memories`/`ports`. Every `sv_lang_kinds::SymbolKind`
235/// this walk can legitimately see in a module/program body is explicitly
236/// listed; anything else falls through to a typed
237/// `ElabError::UnsupportedConstruct`, never a silent skip (ARCHITECTURE §7
238/// item 9). Member kinds that are recognized-but-irrelevant to this task
239/// (continuous assigns, procedural blocks, parameters, nested instances/
240/// generate constructs, …) are listed explicitly too, contributing nothing —
241/// this is the same "known-benign, not a blanket `_`" pattern
242/// `elaborate.rs`'s `collect_instance_children_into` already uses for the
243/// same reason.
244/// A scope's members, split by kind: `(signals, memories, ports)`.
245pub(crate) type ScopeMembers = (Vec<SignalDecl>, Vec<MemoryDecl>, Vec<PortDecl>);
246
247pub(crate) fn build_members<'d>(scope: sv_lang::Symbol<'d>) -> Result<ScopeMembers, ElabError> {
248    use sv_lang_kinds::SymbolKind;
249
250    let mut signals = Vec::new();
251    let mut memories = Vec::new();
252    let mut ports = Vec::new();
253
254    for (index, member) in crate::netlist::state_items(&scope).into_iter().enumerate() {
255        let key = SymbolKey(index as u32);
256        match member.kind() {
257            SymbolKind::Port => {
258                ports.push(build_port_decl(member, key)?);
259            }
260            SymbolKind::Net | SymbolKind::Variable | SymbolKind::FormalArgument => {
261                let ty = member.value_type().ok_or_else(|| {
262                    ElabError::Frontend(format!(
263                        "signal `{}` (kind {:?}) has no resolved value_type()",
264                        member.hierarchical_path(),
265                        member.kind(),
266                    ))
267                })?;
268                if ty.is_unpacked_array() {
269                    memories.push(build_memory_decl(member, key, ty)?);
270                } else {
271                    let kind = if member.kind() == SymbolKind::Net {
272                        SignalKind::Net(net_kind_of_member(scope, &ty))
273                    } else {
274                        SignalKind::Variable
275                    };
276                    signals.push(SignalDecl {
277                        key,
278                        name: member.name().to_string(),
279                        kind,
280                        value_domain: value_domain_of(ty.bit_width(), &member.hierarchical_path())?,
281                        driver_set: Vec::new(),
282                    });
283                }
284            }
285            SymbolKind::Genvar => {
286                signals.push(SignalDecl {
287                    key,
288                    name: member.name().to_string(),
289                    // A `genvar` is an elaboration-time-only 32-bit signed
290                    // integer per LRM §27.5 — no `sv_lang::Type`/`value_type()`
291                    // is associated with it (verified empirically: `value_type()`
292                    // returns `None` for a `Genvar`-kind symbol), so its width
293                    // is this fixed LRM constant, not a frontend-reported one.
294                    value_domain: ValueDomain::Narrow { bits: 32 },
295                    kind: SignalKind::Genvar,
296                    driver_set: Vec::new(),
297                });
298            }
299            // Known-benign, non-signal members a module/program body can
300            // legitimately contain — explicitly enumerated (never a blanket
301            // `_`) so a genuinely unrecognized kind still falls to the error
302            // arm below. `Instance`/`InstanceArray`/`GenerateBlockArray`/
303            // `GenerateBlock` are handled by the separate instance-hierarchy
304            // walk (`elaborate.rs`); this wave's `Template.signals`/
305            // `memories`/`ports` only cover members declared directly in
306            // `scope`'s own flat `members()` list, matching `SymbolKey`
307            // assignment's own flat walk (Task 3) — a signal declared inside
308            // a `generate for` is a known, structural limitation inherited
309            // from that same flat numbering, not something this task's own
310            // walk special-cases away.
311            SymbolKind::MultiPort
312            | SymbolKind::InterfacePort
313            | SymbolKind::Modport
314            | SymbolKind::LetDecl
315            | SymbolKind::MethodPrototype
316            | SymbolKind::PrimitiveInstance
317            | SymbolKind::NetAlias
318            | SymbolKind::ClockingBlock
319            | SymbolKind::ContinuousAssign
320            | SymbolKind::Parameter
321            | SymbolKind::TypeParameter
322            | SymbolKind::ProceduralBlock
323            | SymbolKind::StatementBlock
324            | SymbolKind::Subroutine
325            | SymbolKind::Sequence
326            | SymbolKind::Property
327            // A checker declaration (LRM 17.2) is a definition; its
328            // instances' items are flattened into the scope by
329            // `module_items`.
330            | SymbolKind::Checker
331            | SymbolKind::ElabSystemTask
332            | SymbolKind::TypeAlias
333            // A user-defined nettype (LRM 6.6.7) is a declaration: it has no
334            // storage; its nets are ordinary `Net` members.
335            | SymbolKind::NetType
336            | SymbolKind::DefParam
337            | SymbolKind::Specparam
338            // A specify block contributes no STATE storage of its own
339            // (`docs/superpowers/plans/2026-10-03-sdf.md` Task 4a) — see
340            // `elaborate.rs`'s own, identically-shaped arm for why.
341            | SymbolKind::SpecifyBlock
342            // A covergroup declaration has no storage of its own (its
343            // instances live in the coverage runtime, their handles in the
344            // covergroup variables).
345            | SymbolKind::CovergroupType
346            // A class declared in a module: no module storage of its own.
347            | SymbolKind::ClassType
348            | SymbolKind::GenericClassDef
349            | SymbolKind::ForwardingTypedef
350            | SymbolKind::EmptyMember
351            | SymbolKind::TransparentMember
352            | SymbolKind::DeferredMember
353            | SymbolKind::Attribute
354            | SymbolKind::WildcardImport
355            | SymbolKind::ExplicitImport
356            | SymbolKind::Instance
357            | SymbolKind::InstanceArray
358            | SymbolKind::GenerateBlockArray
359            | SymbolKind::GenerateBlock => {}
360            other => {
361                return Err(ElabError::UnsupportedConstruct {
362                    hier_path: member.hierarchical_path(),
363                    detail: format!(
364                        "{other:?} member not yet lowered into Template.signals/memories/ports"
365                    ),
366                });
367            }
368        }
369    }
370
371    Ok((signals, memories, ports))
372}
373
374/// (NL-037, Task 8/NL-007's own sensitivity assertion) Walks `scope.members()`
375/// — the SAME flat, declaration-order enumeration
376/// `build_members`/`confirm_and_assign_symbol_keys` already walk over the
377/// same canonical body, so `processes[i].key` lines up with that same
378/// `SymbolKey` numbering, never a second, independently-derived index — and
379/// lowers every `ProceduralBlock` member into a `ProcessTemplate`.
380/// Exhaustive over `ProceduralBlockKind`'s 6 documented variants
381/// (`ast.rs:5938-5951`, no `#[non_exhaustive]`): `Initial`/`Final` get
382/// `StaticSensitivity::None` (NL-037's own acceptance); `AlwaysFF` gets a
383/// real `Explicit` derivation from its own root timing control (NL-038's
384/// explicit-sensitivity half — the only half this task's own tests,
385/// NL-007/NL-076/NL-082, exercise). `Always`/`AlwaysComb`/`AlwaysLatch` stay
386/// on the `StaticSensitivity::None` placeholder Task 7 left them on:
387/// deriving their real `Implicit` read-set-based sensitivity is NL-038's
388/// other half, not among this task's assigned ledger IDs (NL-007, NL-063,
389/// NL-076, NL-082) and not exercised by any of their tests — left as
390/// documented remaining work rather than an unverified guess (this
391/// workspace's "no test that would fail if it broke" posture).
392pub(crate) fn build_processes(
393    scope: sv_lang::Symbol<'_>,
394    scope_keys: &HashMap<crate::symid::SymbolId, SymbolKey>,
395) -> Result<Vec<ProcessTemplate>, ElabError> {
396    use sv_lang_kinds::SymbolKind;
397
398    let mut processes = Vec::new();
399    for (index, member) in crate::netlist::module_items(&scope).into_iter().enumerate() {
400        if member.kind() != SymbolKind::ProceduralBlock {
401            continue;
402        }
403        let key = SymbolKey(index as u32);
404        let kind = member.procedural_block_procedure_kind();
405        let sensitivity = match kind {
406            sv_lang::ProceduralBlockKind::Initial | sv_lang::ProceduralBlockKind::Final => {
407                StaticSensitivity::None
408            }
409            sv_lang::ProceduralBlockKind::AlwaysFF => {
410                let stmt = crate::process::cfg::proc_body(&member).ok_or_else(|| {
411                    ElabError::Frontend(format!(
412                        "always_ff `{}` has no body()",
413                        member.hierarchical_path()
414                    ))
415                })?;
416                let tc = stmt
417                    .timing_control()
418                    .ok_or_else(|| ElabError::UnsupportedConstruct {
419                        hier_path: member.hierarchical_path(),
420                        detail: "always_ff block with no root timing control (dynamic/nested \
421                                  sensitivity is not yet lowered by this elaboration pass)"
422                            .to_string(),
423                    })?;
424                if is_inferred_clock_event(tc)
425                    || crate::sva::sampled::mentions_global_clock_event(tc)
426                {
427                    // A checker procedure's `@clock` with `clock` left at its default
428                    // `$inferred_clock` (LRM 17.4) waits on the default clocking of
429                    // the scope that instantiates the checker. Resolving it needs the
430                    // syntax trees, which elaboration does not have, so the lowering
431                    // (`process::cfg`'s `resolve_inferred_clock`) resolves it; this
432                    // deduplicated template carries no static edge for the process,
433                    // which therefore is not seeded into the clock-domain graph.
434                    // `@$global_clock` (14.14) likewise needs the global clocking
435                    // declaration in scope, which only the syntax trees tell;
436                    // so does one member of an event list
437                    // (`@($global_clock or posedge rst)`), which the lowering
438                    // resolves member by member (`process::cfg`).
439                    StaticSensitivity::None
440                } else {
441                    let edges =
442                        explicit_sensitivity_edges(tc, scope_keys, &member.hierarchical_path())?;
443                    if edges.is_empty() {
444                        StaticSensitivity::None
445                    } else {
446                        StaticSensitivity::Explicit { edges }
447                    }
448                }
449            }
450            // NL-038's Implicit half (AlwaysComb/AlwaysLatch) and plain
451            // `always`'s sensitivity are out of this task's scope — see this
452            // function's own doc comment above.
453            sv_lang::ProceduralBlockKind::Always
454            | sv_lang::ProceduralBlockKind::AlwaysComb
455            | sv_lang::ProceduralBlockKind::AlwaysLatch => StaticSensitivity::None,
456        };
457        processes.push(ProcessTemplate {
458            key,
459            kind,
460            sensitivity,
461        });
462    }
463    Ok(processes)
464}
465
466/// Whether `tc` is a lone `@clock` signal event whose `clock` is the call of
467/// `$inferred_clock`, or an event list with such a member (`@(clock or posedge
468/// z)`, resolved member by member in `process::cfg`; see [`build_processes`]).
469fn is_inferred_clock_event(tc: sv_lang::TimingControl<'_>) -> bool {
470    if tc.kind() == sv_lang_kinds::TimingControlKind::EventList {
471        return tc
472            .children()
473            .into_iter()
474            .filter_map(|c| c.as_timing_control())
475            .any(is_inferred_clock_event);
476    }
477    tc.kind() == sv_lang_kinds::TimingControlKind::SignalEvent
478        && tc
479            .children()
480            .into_iter()
481            .find_map(|c| c.as_expression())
482            .is_some_and(|e| crate::process::cfg::is_inferred_clock_call(&e))
483}
484
485/// Task 8 (NL-038's explicit half): recursively lowers a timing control's
486/// own edge-sensitivity entries. Exhaustive over `TimingControlKind` (which
487/// IS `#[non_exhaustive]` upstream — this match still ends in a typed-error
488/// wildcard arm per ARCHITECTURE §7 item 9, never a silent skip):
489/// `SignalEvent` (`@(posedge clk)`) yields one `(SymbolKey, EdgeKind)` pair
490/// from its own edge + its single expression child's referenced symbol;
491/// `EventList` (`@(posedge clk or negedge rst_n)`, `@(a, b)`) recurses into
492/// every child timing control and concatenates. Any other kind
493/// (`Delay`/`ImplicitEvent`/`RepeatedEvent`/…) is a construct this v1
494/// elaboration slice does not lower into an explicit sensitivity list — a
495/// typed `ElabError::UnsupportedConstruct`, never a silent empty list
496/// (which would be indistinguishable from "no sensitivity" and would wrongly
497/// classify a real sequential process as combinational downstream).
498fn explicit_sensitivity_edges(
499    tc: sv_lang::TimingControl<'_>,
500    scope_keys: &HashMap<crate::symid::SymbolId, SymbolKey>,
501    context_hier_path: &str,
502) -> Result<Vec<(SymbolKey, crate::state::EdgeKind)>, ElabError> {
503    use sv_lang_kinds::TimingControlKind;
504
505    match tc.kind() {
506        TimingControlKind::SignalEvent => {
507            // `@$global_clock`, alone or in an event list, never gets here:
508            // `build_processes` leaves it to the lowering.
509            debug_assert!(!crate::sva::sampled::is_global_clock_event(tc));
510            let edge = edge_kind_of(tc.edge());
511            let expr = tc
512                .children()
513                .into_iter()
514                .find_map(|c| c.as_expression())
515                .ok_or_else(|| {
516                    ElabError::Frontend(format!(
517                        "SignalEvent timing control in `{context_hier_path}` has no expression \
518                         child naming the watched signal"
519                    ))
520                })?;
521            // LRM 9.4.2: an event expression may be any expression (`@(!rst)`,
522            // `@(a & b)`): it fires on a value change, with no single watched
523            // signal. Such an entry names no clock for the domain graph, which
524            // reads only the list's first edge as a hint; the lowering waits on
525            // the expression itself, so the entry is left out here.
526            let Some(sym) = expr.referenced_symbol() else {
527                return Ok(Vec::new());
528            };
529            // LRM 25.4/25.5: a signal reached through an interface port (or a
530            // modport of it) lives in the interface instance's scope, not this
531            // body's: like a non-signal expression it names no local clock, and
532            // the lowering waits on the expression itself.
533            if !scope_keys.contains_key(&sym.uid())
534                && !sym
535                    .hierarchical_path()
536                    .starts_with(&format!("{context_hier_path}."))
537            {
538                return Ok(Vec::new());
539            }
540            let key = *scope_keys.get(&sym.uid()).ok_or_else(|| {
541                ElabError::Frontend(format!(
542                    "SignalEvent timing control in `{context_hier_path}` names `{}`, not a \
543                     member of its own scope's SymbolKey table",
544                    sym.hierarchical_path()
545                ))
546            })?;
547            Ok(vec![(key, edge)])
548        }
549        TimingControlKind::EventList => {
550            let mut out = Vec::new();
551            for child in tc.children() {
552                let child_tc = child.as_timing_control().ok_or_else(|| {
553                    ElabError::Frontend(format!(
554                        "EventList timing control in `{context_hier_path}` has a non-timing-\
555                         control child"
556                    ))
557                })?;
558                out.extend(explicit_sensitivity_edges(
559                    child_tc,
560                    scope_keys,
561                    context_hier_path,
562                )?);
563            }
564            Ok(out)
565        }
566        other => Err(ElabError::UnsupportedConstruct {
567            hier_path: context_hier_path.to_string(),
568            detail: format!(
569                "{other:?} timing control kind in an explicit sensitivity list is not yet \
570                 lowered by this elaboration pass"
571            ),
572        }),
573    }
574}
575
576/// Mirrors `sv_lang::EdgeKind`'s four variants onto `crate::state::EdgeKind`
577/// (`state/mod.rs`'s own doc comment: "the `netlist` track owns the real
578/// conversion once it wires the `sv-lang` dependency"). Exhaustive.
579fn edge_kind_of(edge: sv_lang::EdgeKind) -> crate::state::EdgeKind {
580    match edge {
581        sv_lang::EdgeKind::None => crate::state::EdgeKind::None,
582        sv_lang::EdgeKind::PosEdge => crate::state::EdgeKind::Posedge,
583        sv_lang::EdgeKind::NegEdge => crate::state::EdgeKind::Negedge,
584        sv_lang::EdgeKind::BothEdges => crate::state::EdgeKind::BothEdges,
585    }
586}
587
588fn build_port_decl(port: sv_lang::Symbol<'_>, key: SymbolKey) -> Result<PortDecl, ElabError> {
589    let direction = port_direction_of(port.port_direction());
590    let ty = port.port_type().ok_or_else(|| {
591        ElabError::Frontend(format!(
592            "port `{}` has no resolved port_type()",
593            port.hierarchical_path(),
594        ))
595    })?;
596    let bits = u32::try_from(ty.bit_width()).map_err(|_| {
597        ElabError::Frontend(format!(
598            "port `{}` has an out-of-range bit width {}",
599            port.hierarchical_path(),
600            ty.bit_width(),
601        ))
602    })?;
603    Ok(PortDecl {
604        key,
605        name: port.name().to_string(),
606        direction,
607        bits,
608    })
609}
610
611fn build_memory_decl(
612    member: sv_lang::Symbol<'_>,
613    key: SymbolKey,
614    ty: sv_lang::Type<'_>,
615) -> Result<MemoryDecl, ElabError> {
616    let element = ty.element_type().ok_or_else(|| {
617        ElabError::Frontend(format!(
618            "memory `{}` is an unpacked array with no element_type()",
619            member.hierarchical_path(),
620        ))
621    })?;
622    let element_bits = u32::try_from(element.bit_width()).map_err(|_| {
623        ElabError::Frontend(format!(
624            "memory `{}` has an out-of-range element bit width {}",
625            member.hierarchical_path(),
626            element.bit_width(),
627        ))
628    })?;
629    let range = ty.fixed_range();
630    let depth = range
631        .left
632        .checked_sub(range.right)
633        .map(|span| span.unsigned_abs())
634        .and_then(|span| span.checked_add(1))
635        .ok_or_else(|| {
636            ElabError::Frontend(format!(
637                "memory `{}` has an out-of-range depth (range {:?})",
638                member.hierarchical_path(),
639                range,
640            ))
641        })?;
642    Ok(MemoryDecl {
643        key,
644        name: member.name().to_string(),
645        element_bits,
646        depth,
647    })
648}
649
650/// (Load-bearing deviation.) sv-lang 0.1.9's C API exposes no accessor from a
651/// `Net`-kind `Symbol` to its own `NetType` symbol: `NetSymbol::netType`
652/// (`slang::ast::NetSymbol`'s own field, the thing that actually distinguishes
653/// `wire` from `wand`/`wor`/`tri`/…) is never bridged to the C API. Verified
654/// exhaustively against `/root/lab/slang/source/capi/CApiAst.cpp`: every one
655/// of the 6 call sites touching `NetSymbol` there exposes only
656/// `expansionHint`/`getChargeStrength`/`getDelay`/`getDriveStrength`/
657/// `isImplicit` — never the `netType` reference. `Symbol::net_kind()` itself
658/// (`slang_net_type_net_kind`) hard-requires `sym->kind == SymbolKind::NetType`
659/// and returns `NetKind::Unknown` for anything else, including a `Net`-kind
660/// symbol; confirmed empirically too (a scratch integration test compiling
661/// `module m; wire w; wand aw; endmodule` and calling `.net_kind()` on both
662/// `Net`-kind members returned `Unknown` for both).
663///
664/// This pass therefore resolves a net's `NetKind` via the enclosing scope's
665/// `Symbol::default_net_type()`, which is *exactly* correct for an implicit
666/// net (`net_is_implicit()` — LRM-mandated: an implicit net is always the
667/// scope's default net type) and is this wave's only tested/required case
668/// for an explicit declaration too (a bare `wire w;`, matching NL-029's own
669/// fixture, with no `` `default_nettype `` override — confirmed empirically:
670/// `body.default_net_type().unwrap().net_kind() == NetKind::Wire` for exactly
671/// that fixture). An explicit net whose own keyword differs from the scope's
672/// ambient default (e.g. an explicit `wand w;` while the default nettype is
673/// still `wire`) cannot be distinguished with the pinned API — this is a
674/// documented limitation of this wave's implementation, not a silently wrong
675/// answer glossed over: `scope` is always the exact scope `member` was
676/// declared in (`elaborate.rs`'s `dedup_templates` calls [`build_members`]
677/// with the canonical instance body itself as `scope`), so this is never
678/// weaker than "the correct answer for every net this wave's ledger rows
679/// actually require."
680fn net_kind_of(scope: sv_lang::Symbol<'_>) -> sv_lang::NetKind {
681    scope
682        .default_net_type()
683        .map(|net_type| net_type.net_kind())
684        .unwrap_or(sv_lang::NetKind::Unknown)
685}
686
687/// (Task 10, NL-117 — additive to `net_kind_of`'s own documented deviation,
688/// not a replacement for it.) `net_kind_of`'s scope-default heuristic cannot
689/// distinguish an EXPLICIT `interconnect w;` declaration from the enclosing
690/// scope's ambient default net type (the same load-bearing API gap
691/// documented on `net_kind_of` above) — EXCEPT that `interconnect` is
692/// independently, reliably detectable a different way: LRM-mandated,
693/// `interconnect` nets have no fixed data type until resolved by connection,
694/// and this pinned `sv-lang = "=0.1.9"` represents that as a 0-bit placeholder
695/// `Type` on the net's own `value_type()` — confirmed empirically this task
696/// (`module m; interconnect w; wire ordinary; wire [3:0] wide; endmodule`,
697/// `has_errors() == false`): `w.value_type().unwrap().bit_width() == 0`
698/// while `ordinary`'s is `1` and `wide`'s is `4`. Every OTHER LRM net-type
699/// keyword (`wire`/`wand`/`wor`/`tri`/…) always carries a concrete >=1-bit
700/// packed data type, so a 0-bit `Type` on a `Net`-kind symbol's own
701/// `value_type()` cannot arise from any case other than `interconnect` — this
702/// check is therefore checked FIRST and is exact (never a guess), falling
703/// back to [`net_kind_of`]'s existing (still-limited, still-documented)
704/// scope-default heuristic for every other case, which this task does not
705/// change.
706fn net_kind_of_member(scope: sv_lang::Symbol<'_>, ty: &sv_lang::Type<'_>) -> sv_lang::NetKind {
707    if ty.bit_width() == 0 {
708        return sv_lang::NetKind::Interconnect;
709    }
710    net_kind_of(scope)
711}
712
713fn port_direction_of(direction: sv_lang::ArgumentDirection) -> PortDirection {
714    match direction {
715        sv_lang::ArgumentDirection::In => PortDirection::Input,
716        sv_lang::ArgumentDirection::Out => PortDirection::Output,
717        sv_lang::ArgumentDirection::InOut => PortDirection::Inout,
718        sv_lang::ArgumentDirection::Ref => PortDirection::Ref,
719    }
720}
721
722fn value_domain_of(bits: u64, hier_path: &str) -> Result<ValueDomain, ElabError> {
723    let bits32 = u32::try_from(bits).map_err(|_| {
724        ElabError::Frontend(format!(
725            "signal `{hier_path}` has an out-of-range bit width {bits}"
726        ))
727    })?;
728    Ok(if bits32 <= 64 {
729        ValueDomain::Narrow { bits: bits32 }
730    } else {
731        ValueDomain::Wide { bits: bits32 }
732    })
733}