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}