vj_sim/netlist/elaborate.rs
1//! The deterministic DFS elaboration entry point (NL-001, NL-002, NL-008,
2//! NL-010, NL-021). Frontend reuse only: this module consumes
3//! `sv_lang::Design` (via `EvalSession::design()`) directly — there is no
4//! parallel parse/elaborate path anywhere in this crate.
5//!
6//! Call order (binding for every caller, including this module's own tests
7//! and doctest — see the plan's "Load-bearing lifetime discovery"):
8//! ```
9//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
10//! use sv_lang::{Compilation, Session};
11//! use vj_sim::netlist;
12//!
13//! let session = Session::new();
14//! let mut comp = Compilation::new(&session)?;
15//! comp.add_source("module m; endmodule\n")?;
16//! let mut design = comp.compile()?; // owned Design
17//! let eval = design.eval_session(); // the ONE &mut borrow, taken first
18//! let netlist = netlist::elaborate(&eval)?; // elaborate() takes `eval` itself
19//! assert_eq!(netlist.top.len(), 1);
20//! # Ok(()) }
21//! ```
22
23use crate::symid::SymbolUid;
24use std::collections::HashMap;
25
26use crate::netlist::dedup::{DefinitionTable, template_key_of};
27use crate::netlist::exprwalk::expression_read_set;
28use crate::netlist::instance::array_path_of;
29use crate::netlist::lattice::cont_assign_read_set;
30use crate::netlist::template::{build_members, build_processes};
31use crate::netlist::{
32 ContAssignTemplate, ElabError, InstanceId, InstanceNode, InstanceTree, InstantiatedSignalId,
33 NetlistView, PortDirection, SubInstanceSlot, SymbolKey, Template, TemplateId, TemplateKey,
34 confirm_and_assign_symbol_keys, merge_groups_within_bucket,
35};
36
37/// The hand-off value `elaborate()` produces: the full undeduped
38/// `InstanceTree` plus (this task) a trivial 1:1 `Template` per instance.
39pub struct Netlist<'d> {
40 pub templates: Vec<Template>,
41 pub instances: InstanceTree,
42 pub top: Vec<InstanceId>,
43 /// `hierarchical_path()` cache (NL-021): computed exactly once per
44 /// instance during the DFS below, index-aligned with `InstanceId.0`,
45 /// never rebuilt on repeated `NetlistView::hierarchical_path` calls.
46 hier_paths: Vec<String>,
47 /// Declared here (per the plan's Task 1 spec) so `Netlist`'s shape is
48 /// stable across tasks. Populated by `dedup_templates` (Task 3) — the one
49 /// place a live per-instance, per-member `Symbol<'d>` is captured, for
50 /// Task 8's driver-graph construction. Index-aligned with `InstanceId.0`
51 /// (one entry per staged instance, whether or not it merged with
52 /// another); each inner `Vec` is `SymbolKey`-ordered. Read only through
53 /// `Netlist::member_symbol` below.
54 member_symbols: Vec<Vec<sv_lang::Symbol<'d>>>,
55 /// Task F3 (`docs/superpowers/plans/2026-09-27-wave-A2-slice-finish.md`,
56 /// CG-125/NL-035): the real `Instance`-kind `sv_lang::Symbol<'d>` for
57 /// each elaborated instance, index-aligned with `InstanceId.0` — pushed
58 /// in `visit_instance` at the same point `staged`/`hier_paths` are
59 /// pushed, never re-derived later. Unlike `member_symbols` (which only
60 /// carries a *deduped Template member's* per-instance symbol, addressed
61 /// by `SymbolKey`), this is the instantiation site itself: the one
62 /// handle the run pipeline needs to walk `Symbol::instance_port_connections()`
63 /// (each port's own formal `Symbol` plus its parent-scope binding
64 /// `Expression`) and `Symbol::instance_body()` (that instance's own,
65 /// undeduped procedural-block/signal members) to build per-instance
66 /// state and synthetic port-connection processes. Exposed via
67 /// [`Netlist::instance_symbol`].
68 instance_syms: Vec<sv_lang::Symbol<'d>>,
69 /// Task 6 (NL-026): the design's global time scale — see
70 /// `resolve_time_scale`'s doc comment for exactly where this comes from
71 /// (a documented deviation from the plan's literal spec).
72 pub time_scale: sv_lang::TimeScale,
73 /// Task 6 (NL-025): the tick-per-nanosecond normalization factor SCHED
74 /// reads as its sole source of truth — see `tick_ns_per_unit_of`.
75 pub tick_ns_per_unit: u64,
76 _design: std::marker::PhantomData<&'d sv_lang::Design>,
77}
78
79/// Folds every unpacked-struct member's default value (`struct { bit [3:0]
80/// lo = 5; } s;`, LRM 1800-2023 7.2.2) so the storage layout can read it:
81/// slang prefolds variable initializers but leaves a member's bare literal
82/// unfolded, and evaluating it caches the value on the node.
83fn fold_struct_member_defaults(eval: &sv_lang::EvalSession<'_>) {
84 fn sweep(ty: sv_lang::Type<'_>, eval: &sv_lang::EvalSession<'_>, depth: u32) {
85 if depth > 8 || !ty.is_unpacked_struct() {
86 return;
87 }
88 for f in ty.fields() {
89 if let Some(init) = f.initializer() {
90 let _ = eval.eval_constant(init);
91 }
92 if let Some(fty) = f.value_type() {
93 sweep(fty, eval, depth + 1);
94 }
95 }
96 }
97 eval.design().root().visit(|s| {
98 if let Some(t) = s.declared_type() {
99 sweep(t.canonical(), eval, 0);
100 }
101 sv_lang::Walk::Continue
102 });
103}
104
105/// Applies the IEEE 1800-2023 18.5.1 constraint-prototype checks to every
106/// class declaration reachable from the root, including classes that are
107/// never instantiated (an unused `extern constraint c;` is still an error).
108fn check_declared_class_constraints(eval: &sv_lang::EvalSession<'_>) -> Result<(), ElabError> {
109 let mut result = Ok(());
110 eval.design().root().visit(|s| {
111 if s.kind() == sv_lang_kinds::SymbolKind::ClassType
112 && let Err(e) = super::classes::check_class_constraints(s)
113 {
114 result = Err(e);
115 return sv_lang::Walk::Break;
116 }
117 sv_lang::Walk::Continue
118 });
119 result
120}
121
122/// Deterministic DFS over `Design::top_instances()` in declaration order —
123/// this order **is** SCHED's `registration_order` (ARCHITECTURE §6.5; no
124/// separate sort pass exists anywhere in this pipeline). Never panics on
125/// malformed/unsupported-but-valid input: every failure path returns a typed
126/// `ElabError` (NL-008).
127///
128/// Takes the `EvalSession` itself (not a bare `&'d Design`): later tasks in
129/// this same pipeline (Task 7) need `EvalSession::run_dataflow` from inside
130/// this same function, and `Design::eval_session` requires `&mut Design`, so
131/// the one `&mut` borrow it performs must happen exactly once, before
132/// `elaborate()` is ever called — see the module doc's call-order example.
133///
134/// Deviation from the plan's literal signature (`eval: &sv_lang::EvalSession<'d>`,
135/// with the outer reference's own lifetime left elided): `EvalSession::design(&self)
136/// -> &Design` ties its return value's lifetime to the `&self` borrow used for
137/// *that* call, per ordinary Rust elision — not to `EvalSession`'s own `'d`
138/// parameter. With the outer reference elided (effectively some anonymous,
139/// caller-chosen, possibly-shorter lifetime), every `Symbol` obtained via
140/// `eval.design()...` inside this function would carry that shorter lifetime,
141/// not `'d`, and could never be returned inside `Netlist<'d>` (needed once
142/// `member_symbols: Vec<Vec<Symbol<'d>>>` is actually populated, Task 3).
143/// Pinning the outer reference to `'d` too (`&'d EvalSession<'d>`) makes
144/// `eval.design()` reborrow at exactly `'d`, so every `Symbol` this function
145/// obtains is genuinely `Symbol<'d>` — the plan's own intent, just spelled
146/// out explicitly rather than relying on elision to reach it.
147pub fn elaborate<'d>(eval: &'d sv_lang::EvalSession<'d>) -> Result<Netlist<'d>, ElabError> {
148 // Task 6 (NL-025, NL-026): computed once, up front — independent of the
149 // DFS/dedup state built below (both only read `eval.design().
150 // top_instances()`, never anything `visit_instance`/`dedup_templates`
151 // produce) — and stored directly into the `Netlist` literal so there is
152 // no separate "placeholder then overwrite" step to forget.
153 let time_scale = resolve_time_scale(eval);
154 let tick_ns_per_unit = tick_ns_per_unit_of(time_scale.precision)?;
155 fold_struct_member_defaults(eval);
156 check_declared_class_constraints(eval)?;
157
158 let mut netlist = Netlist {
159 templates: Vec::new(),
160 instances: InstanceTree::new(),
161 top: Vec::new(),
162 hier_paths: Vec::new(),
163 member_symbols: Vec::new(),
164 instance_syms: Vec::new(),
165 time_scale,
166 tick_ns_per_unit,
167 _design: std::marker::PhantomData,
168 };
169 // Staging list of (Instance symbol, InstanceBody symbol), index-aligned
170 // with `InstanceId.0` — every reachable instance body is visited and
171 // recorded here BEFORE Task 2's dedup pass runs below (NL-014: walk
172 // before collapse). Task 2's own bucketing needs both handles per
173 // instance: `body` for `instance_body_definition()`/
174 // `instance_body_has_same_type`, `instance` for `parameters()` (see
175 // `dedup::template_key_of`'s doc comment for why `body` alone is not
176 // enough).
177 let mut staged: Vec<(sv_lang::Symbol<'d>, sv_lang::Symbol<'d>)> = Vec::new();
178
179 for top_sym in eval.design().top_instances() {
180 match top_sym.kind() {
181 sv_lang_kinds::SymbolKind::Instance => {
182 let id = visit_instance(&mut netlist, &mut staged, top_sym, None)?;
183 netlist.top.push(id);
184 }
185 other => {
186 // Defensive: `Design::top_instances()` is documented to
187 // yield only module/program `Instance` symbols, but this
188 // pass never trusts that silently — an unrecognized
189 // top-level symbol kind is a typed error, not a panic or a
190 // silently-dropped top module.
191 return Err(ElabError::UnsupportedConstruct {
192 hier_path: top_sym.hierarchical_path(),
193 detail: format!(
194 "top-level symbol of kind {other:?} is not a module/program instance"
195 ),
196 });
197 }
198 }
199 }
200
201 dedup_templates(&mut netlist, &staged, eval)?;
202
203 Ok(netlist)
204}
205
206/// Task 7 (NL-034, NL-035): builds a `SymbolId -> SymbolKey` table for one
207/// scope, keyed by `Symbol::id()` (the hierarchical-path-keyed, context-free
208/// stable identity `sv-lang` itself publishes — see `lattice.rs`'s own doc
209/// comment for why this indirection, rather than a `SymbolKey` inside the
210/// `Lattice`/expression-walker themselves, is required). Position-based:
211/// `scope.members().enumerate()`, the SAME flat walk `build_members`/
212/// `confirm_and_assign_symbol_keys` already perform over the same canonical
213/// body, so a key this table produces always equals the `SymbolKey` that
214/// body's own `Template`/`SignalDecl`/`PortDecl`/`ProcessTemplate` entries
215/// carry for the same logical member — never a second, independently
216/// re-derived numbering. Valid for ANY instance body (not only a group's
217/// canonical member): dedup's merge invariant (Task 3) guarantees every
218/// body in a merge group enumerates its members in the identical
219/// (kind, name) order, so the k-th member of ANY of them is "the same"
220/// logical member as the k-th member of the canonical body, and gets the
221/// same `SymbolKey`.
222///
223/// **Load-bearing finding, TDD'd against real `sv-lang` output (verified
224/// this task): an ANSI port declaration (`input logic a`) produces TWO
225/// members in `scope.members()` — a `Port`-kind symbol AND its own backing
226/// `Net`/`Variable`-kind symbol, immediately adjacent, SHARING THE SAME
227/// `SymbolId`** (both have hierarchical path `"m.a"` — `SymbolId` is only
228/// the path string, not a true per-AST-node identity). Because `HashMap`'s
229/// `FromIterator`/`insert` keeps the LAST value written for a duplicate
230/// key, and the backing variable always enumerates immediately AFTER its
231/// Port counterpart, this table resolves a colliding `SymbolId` to the
232/// BACKING VARIABLE's `SymbolKey` — which is exactly the symbol an ordinary
233/// identifier reference inside an expression (`assign y = a & b;`'s `a`/
234/// `b`/`y`, or a port-connection's bound actual expression) resolves to via
235/// `Expression::referenced_symbol()`/`DfaEvent::symbol` (never the `Port`
236/// symbol itself — a module body's expressions never "read" the port
237/// declaration node). This table is therefore correct, unmodified, for
238/// every caller in this module EXCEPT resolving `PortConnection::port()`
239/// itself (a `Port`-kind symbol by definition) back to its own `SymbolKey`
240/// — that one case is resolved separately, by name-match against the
241/// already-built `Template.ports` (see the port-connection loop below),
242/// never through this table.
243pub(crate) fn symbol_key_map(
244 scope: sv_lang::Symbol<'_>,
245) -> HashMap<crate::symid::SymbolId, SymbolKey> {
246 crate::netlist::state_items(&scope)
247 .into_iter()
248 .enumerate()
249 .map(|(i, m)| (m.uid(), SymbolKey(i as u32)))
250 .collect()
251}
252
253/// Task 6 (NL-026): resolves the design's global `sv_lang::TimeScale`.
254///
255/// **Deviation from the plan's literal spec, documented here (binding for
256/// both this function and `tick_ns_per_unit_of` below):** the plan cites
257/// `Design::default_time_scale()` as the source. Verified directly against
258/// sv-lang's own test suite
259/// (`/root/lab/slang/rust/sv-lang/tests/semantic.rs`'s
260/// `default_time_scale_is_none_when_not_configured`) and its own doc comment
261/// (`ast.rs:1678-1703`) that this getter is *always* `None` at the pinned
262/// `sv-lang = "=0.1.9"` version: sv-lang's `Options` builder never sets
263/// `CompilationOptions::defaultTimeScale` (no C-API setter exists for it),
264/// and — the load-bearing fact — it is documented to NOT be derived from
265/// `` `timescale `` source directives at all (a separate, per-scope/
266/// per-`Definition` mechanism). Using it would panic (`.unwrap()` on `None`)
267/// for every real design and could never differ between two designs whose
268/// only difference is their `` `timescale `` directive, which is exactly
269/// what NL-025's own acceptance criterion requires. The real per-design
270/// "timescale/timeprecision directives" both NL-025 and NL-026 ask for live
271/// on `Scope::time_scale()` (`ast.rs:10422-10451`, "the time scale for delay
272/// values expressed within this scope... walks the parent-scope chain") —
273/// read here off the design's first top instance's own body scope, the same
274/// "the design's own scope" this pass already treats as canonical elsewhere
275/// (e.g. `resolve_hier_path`'s top-instance walk). Falls back to the LRM
276/// §3.14.2.2 default (`1ns` base and precision) only when no `` `timescale ``
277/// directive applies anywhere in the design and no compilation default is
278/// configured either (`Scope::time_scale()` returns `None`) — never a panic,
279/// never a silently-wrong zero value.
280fn resolve_time_scale(eval: &sv_lang::EvalSession<'_>) -> sv_lang::TimeScale {
281 let from_top_scope = eval
282 .design()
283 .top_instances()
284 .next()
285 .and_then(|top| top.instance_body())
286 .and_then(|body| body.time_scale());
287
288 from_top_scope.unwrap_or(sv_lang::TimeScale {
289 base: sv_lang::TimeScaleValue {
290 unit: sv_lang::TimeUnit::Nanoseconds,
291 magnitude: 1,
292 },
293 precision: sv_lang::TimeScaleValue {
294 unit: sv_lang::TimeUnit::Nanoseconds,
295 magnitude: 1,
296 },
297 })
298}
299
300/// Task 6 (NL-025): derives the tick-per-nanosecond normalization factor
301/// (Decision 8) from `time_scale.precision` — the finest unit any `#`/`##`
302/// delay in the design can express. One "tick" is one unit of that
303/// precision; this returns how many ticks make up one nanosecond (e.g. a
304/// `1ps` precision means 1000 ticks per ns, since `1ns == 1000ps`). Both the
305/// tick's own length and one nanosecond are first expressed in femtoseconds
306/// — finer than every named `TimeUnit` this function handles, so the
307/// division below is always between two whole numbers of femtoseconds, never
308/// a lossy float — then divided.
309///
310/// Exhaustive over `TimeUnit` (`#[non_exhaustive]` upstream, so this must
311/// end in a wildcard arm to compile at all): a raw unit value sv-lang itself
312/// doesn't yet name (`TimeUnit::Other`) is a typed `ElabError::Frontend`,
313/// never a silent default/zero (ARCHITECTURE §7 item 9) — likewise a
314/// magnitude `0`, or a precision finer than 1ns that does not divide it
315/// (a precision coarser than 1ns floors at one tick per nanosecond);
316/// returning a silently-truncated/rounded value there would be exactly the
317/// kind of "gate that cannot fail" this workspace has already been bitten by
318/// once (`gates-that-cannot-fail`).
319fn tick_ns_per_unit_of(precision: sv_lang::TimeScaleValue) -> Result<u64, ElabError> {
320 use sv_lang::TimeUnit;
321
322 let fs_per_unit: u64 = match precision.unit {
323 TimeUnit::Seconds => 1_000_000_000_000_000,
324 TimeUnit::Milliseconds => 1_000_000_000_000,
325 TimeUnit::Microseconds => 1_000_000_000,
326 TimeUnit::Nanoseconds => 1_000_000,
327 TimeUnit::Picoseconds => 1_000,
328 TimeUnit::Femtoseconds => 1,
329 other => {
330 return Err(ElabError::Frontend(format!(
331 "unrecognized TimeUnit {other:?} in design's time-scale precision"
332 )));
333 }
334 };
335
336 const FS_PER_NS: u64 = 1_000_000;
337 let tick_length_fs = fs_per_unit
338 .checked_mul(precision.magnitude as u64)
339 .ok_or_else(|| {
340 ElabError::Frontend(format!(
341 "time-scale precision magnitude {} overflows u64 femtoseconds",
342 precision.magnitude
343 ))
344 })?;
345
346 // A precision coarser than 1ns (`1us`, `1ms`) needs no sub-nanosecond tick:
347 // the factor floors at one tick per nanosecond (the tick length is the
348 // tool's choice, LRM 3.14.3; delays still scale by their own unit).
349 if tick_length_fs > FS_PER_NS && tick_length_fs.is_multiple_of(FS_PER_NS) {
350 return Ok(1);
351 }
352 if tick_length_fs == 0 || !FS_PER_NS.is_multiple_of(tick_length_fs) {
353 return Err(ElabError::Frontend(format!(
354 "design time-scale precision ({:?} x{}) does not evenly divide 1ns into a \
355 whole number of ticks",
356 precision.unit, precision.magnitude
357 )));
358 }
359
360 Ok(FS_PER_NS / tick_length_fs)
361}
362
363/// Task 2 (NL-004, NL-011, NL-012, NL-013): the hash-then-verify dedup pass.
364/// Tags every staged `(instance, body)` pair with its `TemplateKey`
365/// (`dedup::template_key_of`), calls `dedup::merge_groups_within_bucket`
366/// ONCE over the full tagged list — the exact function
367/// `tests/netlist_dedup.rs`'s forced-collision test also calls directly, so
368/// there is never a second, independently-reasoned merge implementation —
369/// and builds the final, deduped `templates: Vec<Template>`, re-pointing
370/// every merged instance's `InstanceNode.template` at the group's shared
371/// `TemplateId`. `InstanceTree` itself (`netlist.instances`) is never
372/// touched here: dedup collapses `Template`s, never `InstanceNode`s
373/// (NL-011/NL-012).
374///
375/// Task 3 (NL-015, NL-016, NL-018) additions: for every group (including
376/// singleton groups — a `Template` used by exactly one instance still goes
377/// through this same call, with `others: &[]`), calls
378/// `confirm_and_assign_symbol_keys` on the group's bodies — the defensive,
379/// fuzz-testable re-check that hard-fails (`ElabError::MergeInvariantViolated`,
380/// propagated via `?`, never a panic or silent corruption) if any member
381/// disagrees even though `merge_groups_within_bucket`'s own
382/// `instance_body_has_same_type` oracle already said the group's bodies
383/// match — and records each body's own `Vec<Symbol<'d>>` into
384/// `netlist.member_symbols` at that body's own `InstanceId` slot,
385/// `SymbolKey`-index-aligned with `confirm_and_assign_symbol_keys`' `shapes`.
386///
387/// Task 4 (NL-121) addition: a second pass, after every group has assigned
388/// its members' final `InstanceNode.template`, builds each `Template`'s own
389/// `sub_instances` from its canonical instance's `children`. This MUST be a
390/// second pass, not folded into the loop above: `merge_groups_within_bucket`
391/// emits groups in first-seen-`TemplateKey` order (== the order of each
392/// group's lowest-`InstanceId` member), and a parent instance always has a
393/// lower `InstanceId` than any of its children (DFS pre-order numbering), so
394/// a parent's `Template` can be — and often is — constructed before its
395/// children's own groups have run, leaving their `InstanceNode.template`
396/// still unresolved at that point. Every merged instance in one group is
397/// already known (by `confirm_and_assign_symbol_keys`'s member-for-member
398/// confirmation) to have a structurally identical child-instantiation list,
399/// so reading it off the canonical instance alone is sufficient — no other
400/// group member needs to be consulted.
401fn dedup_templates<'d>(
402 netlist: &mut Netlist<'d>,
403 staged: &[(sv_lang::Symbol<'d>, sv_lang::Symbol<'d>)],
404 eval: &'d sv_lang::EvalSession<'d>,
405) -> Result<(), ElabError> {
406 let mut defs = DefinitionTable::default();
407 let tagged: Vec<(TemplateKey, sv_lang::Symbol<'d>)> = staged
408 .iter()
409 .map(|(instance, body)| (template_key_of(&mut defs, *instance, *body), *body))
410 .collect();
411
412 let groups = merge_groups_within_bucket(&tagged);
413
414 netlist.member_symbols = vec![Vec::new(); staged.len()];
415 // Index-aligned with `netlist.templates`: which `InstanceId` was each
416 // `Template`'s canonical (group[0]) member, for the `sub_instances`
417 // second pass below.
418 let mut canonical_instance_of_template: Vec<InstanceId> = Vec::new();
419
420 for group in groups {
421 let canonical_index = group[0];
422 let (canonical_instance, canonical_body) = staged[canonical_index];
423 let key = tagged[canonical_index].0;
424 let definition_name = canonical_instance
425 .instance_definition()
426 .map(|def| def.name().to_string())
427 .unwrap_or_default();
428
429 let other_bodies: Vec<sv_lang::Symbol<'d>> = group[1..]
430 .iter()
431 .map(|&member_index| staged[member_index].1)
432 .collect();
433 let confirmed = confirm_and_assign_symbol_keys(canonical_body, &other_bodies)?;
434 debug_assert_eq!(
435 confirmed.per_body_symbols.len(),
436 group.len(),
437 "one per_body_symbols entry per group member (canonical + every other)"
438 );
439
440 // Task 5 (NL-029, NL-030, NL-120, NL-122): `signals`/`memories`/
441 // `ports` are built from the SAME canonical body `confirm_and_assign_
442 // symbol_keys` just walked above, over the identical flat
443 // `members()` enumeration order, so `SymbolKey` assignment is never
444 // independently re-derived between the two passes.
445 let (signals, memories, ports) = build_members(canonical_body)?;
446
447 // Task 7 (NL-034, NL-037): `processes` (initial/final ->
448 // StaticSensitivity::None; always* -> the same placeholder until
449 // Task 8) and `continuous_assigns` (this Template's OWN `assign`
450 // statements only — synthetic port-connection entries are appended
451 // by the second pass below, once every Template in the design has
452 // been built). Both walk the SAME canonical body as `build_members`
453 // above, over the SAME `SymbolKey` numbering (`scope_keys`).
454 let scope_keys = symbol_key_map(canonical_body);
455 let processes = build_processes(canonical_body, &scope_keys)?;
456 let continuous_assigns = build_own_cont_assigns(eval, canonical_body, &scope_keys)?;
457
458 let template_id = TemplateId(netlist.templates.len() as u32);
459 netlist.templates.push(Template {
460 key,
461 definition_name,
462 signals,
463 memories,
464 processes,
465 continuous_assigns,
466 // Filled in by the second pass below, once every instance's
467 // `InstanceNode.template` in this whole design is resolved.
468 sub_instances: Vec::new(),
469 ports,
470 });
471 canonical_instance_of_template.push(InstanceId(canonical_index as u32));
472
473 for (slot, &member_index) in group.iter().enumerate() {
474 let instance_id = InstanceId(member_index as u32);
475 netlist.instances.node_mut(instance_id).template = template_id;
476 netlist.member_symbols[member_index] = confirmed.per_body_symbols[slot].clone();
477 }
478 }
479
480 for (template_index, &canonical_id) in canonical_instance_of_template.iter().enumerate() {
481 let child_ids = netlist.instances.node(canonical_id).children.clone();
482
483 // Task 7 (NL-035, NL-121): the PARENT scope's own SymbolKey table
484 // — built the same way `build_own_cont_assigns` built one for this
485 // Template's own real `assign` statements above — reused here to
486 // resolve every child port connection's driving-expression
487 // read-set at THIS (parent) scope.
488 let canonical_instance_sym = netlist.instance_symbol(canonical_id);
489 let canonical_body = canonical_instance_sym.instance_body().ok_or_else(|| {
490 ElabError::Frontend(format!(
491 "instance `{}` has no instance_body()",
492 canonical_instance_sym.hierarchical_path()
493 ))
494 })?;
495 let parent_scope_keys = symbol_key_map(canonical_body);
496
497 let mut sub_instances = Vec::with_capacity(child_ids.len());
498 let mut synthetic_assigns = Vec::new();
499
500 for &child_id in &child_ids {
501 let child_template_id = netlist.instances.node(child_id).template;
502 let child_instance_sym = netlist.instance_symbol(child_id);
503
504 let mut port_bindings = Vec::new();
505 for conn in child_instance_sym.instance_port_connections() {
506 let port_symbol = conn.port();
507 // An interface port carries no value of its own: the child
508 // reaches the connected interface instance's members
509 // directly (`b.valid` resolves to `top.bi.valid`).
510 if port_symbol.kind() == sv_lang_kinds::SymbolKind::InterfacePort {
511 continue;
512 }
513 // LRM 23.2.2.1 port expressions: a port that is a part-select,
514 // a concatenation (`.p({a, b})`, `{c, d}`) or otherwise not a
515 // plain reference to one internal signal is not modelled; the
516 // port connection would write the whole internal signal
517 // (wrong width) or leave it without a `PortDecl`.
518 let plain_internal = port_symbol.kind() == sv_lang_kinds::SymbolKind::Port
519 && port_symbol.port_internal_expr().is_none_or(|e| {
520 matches!(
521 e.kind(),
522 sv_lang_kinds::ExpressionKind::NamedValue
523 | sv_lang_kinds::ExpressionKind::ElementSelect
524 | sv_lang_kinds::ExpressionKind::RangeSelect
525 )
526 });
527 if !plain_internal {
528 return Err(ElabError::Frontend(format!(
529 "unsupported: port expression on `{}` (LRM 23.2.2.1: a port that is a \
530 part-select or concatenation of internal signals, or a multi-signal \
531 port, is not supported; declare one port per signal)",
532 port_symbol.hierarchical_path(),
533 )));
534 }
535
536 // (Load-bearing finding, TDD'd against real `sv-lang`
537 // output: an ANSI port declaration produces TWO members in
538 // its owning scope — a `Port`-kind symbol AND its own
539 // backing `Net`/`Variable`-kind symbol — and the two SHARE
540 // the identical `SymbolId` (hierarchical path), since
541 // `SymbolId` is only the path string, not a true per-node
542 // identity. `conn.port()` always returns the `Port`-kind
543 // symbol (its own doc comment: "a Port, MultiPort, or
544 // InterfacePort symbol"), so resolving it through a
545 // `SymbolId`-keyed table (`symbol_key_map`, "last write
546 // wins") would silently return the BACKING VARIABLE's key
547 // instead — wrong for `target`/`port_bindings`, which must
548 // be the port's OWN `SymbolKey` (matching `PortDecl.key`,
549 // itself `Port`-kind-only). A name-match against the
550 // child's own already-built `Template.ports` (unambiguous:
551 // port names are unique within one port list) sidesteps
552 // the collision entirely and gives `key`+`direction` in one
553 // lookup — this is also where `is_output_port_connection`
554 // (direction) comes from, per this task's own doc comment
555 // on why direction is `PortDecl.direction`-derived, never
556 // `Analysis`-dependent. Every child `Template` is already
557 // fully materialized by this point (this whole pass runs
558 // after the per-group loop above finished building every
559 // `Template`, including this child's), so this lookup can
560 // never race the child's own construction.
561 let child_template = &netlist.templates[child_template_id.0 as usize];
562 let (child_port_key, direction) = match child_template
563 .ports
564 .iter()
565 .find(|p| p.name == port_symbol.name())
566 {
567 Some(p) => (p.key, p.direction),
568 // LRM 23.2.2.1: a port expression `.a({x, y})` is one
569 // external port; slang splits its connection into one
570 // per internal `Port` (`y <- w[0]`, `x <- w[11:1]`).
571 // Those internal ports are not body members, but each
572 // is backed by the body's own net/variable of the same
573 // name, which the connection drives.
574 None => {
575 let direction = match port_symbol.port_direction() {
576 sv_lang::ArgumentDirection::In => PortDirection::Input,
577 sv_lang::ArgumentDirection::Out => PortDirection::Output,
578 sv_lang::ArgumentDirection::InOut => PortDirection::Inout,
579 sv_lang::ArgumentDirection::Ref => PortDirection::Ref,
580 };
581 let signal = child_template
582 .signals
583 .iter()
584 .find(|sig| sig.name == port_symbol.name())
585 .ok_or_else(|| {
586 ElabError::Frontend(format!(
587 "port connection `{}` has no matching PortDecl in its \
588 child Template (a concatenated port expression such as \
589 `.p({{a, b}})`, LRM 23.2.2.1, is not supported)",
590 port_symbol.hierarchical_path(),
591 ))
592 })?;
593 (signal.key, direction)
594 }
595 };
596
597 // NL-035: the connection's bound expression, resolved at
598 // the PARENT scope — `None` (an unconnected port) yields an
599 // empty read_set, never an error (a deliberately left-open
600 // port is valid SV, not a construct this pass fails on).
601 let read_set = match conn.expression() {
602 Some(expr) => expression_read_set(expr, &parent_scope_keys),
603 None => Vec::new(),
604 };
605 port_bindings.push((child_port_key, read_set.clone()));
606
607 // A `ref` port (LRM 23.3.3.2) is an alias of its actual, not
608 // a continuous assignment into it: nothing drives the port.
609 if direction == PortDirection::Ref {
610 continue;
611 }
612
613 synthetic_assigns.push(ContAssignTemplate {
614 target: child_port_key,
615 read_set,
616 is_output_port_connection: direction == PortDirection::Output,
617 });
618 }
619
620 sub_instances.push(SubInstanceSlot {
621 port_bindings,
622 child_template: child_template_id,
623 });
624 }
625
626 netlist.templates[template_index].sub_instances = sub_instances;
627 netlist.templates[template_index]
628 .continuous_assigns
629 .extend(synthetic_assigns);
630 }
631
632 Ok(())
633}
634
635/// Task 7 (NL-034): lowers every real `SymbolKind::ContinuousAssign` member
636/// of `scope` (the Template's OWN `assign` statements — never a synthetic
637/// port-connection entry, `is_output_port_connection` is always `false`
638/// here) into a `ContAssignTemplate`. `target` is the assignment's LHS
639/// symbol (`continuous_assign_assignment().left().referenced_symbol()`);
640/// `read_set` is the RHS read-set via [`cont_assign_read_set`]
641/// (`ReadSetLattice`/`run_dataflow`, Decision 1) — `run_dataflow` accepts
642/// the `ContinuousAssign` symbol itself as a "procedure" per its own doc
643/// comment, so no separate RHS-only symbol needs to be located.
644fn build_own_cont_assigns<'d>(
645 eval: &sv_lang::EvalSession<'d>,
646 scope: sv_lang::Symbol<'d>,
647 scope_keys: &HashMap<crate::symid::SymbolId, SymbolKey>,
648) -> Result<Vec<ContAssignTemplate>, ElabError> {
649 use sv_lang_kinds::SymbolKind;
650
651 let mut out = Vec::new();
652 for member in crate::netlist::module_items(&scope) {
653 if member.kind() != SymbolKind::ContinuousAssign {
654 continue;
655 }
656
657 let assignment = member.continuous_assign_assignment().ok_or_else(|| {
658 ElabError::Frontend(format!(
659 "continuous assign `{}` has no continuous_assign_assignment()",
660 member.hierarchical_path()
661 ))
662 })?;
663 let lhs = assignment.left().ok_or_else(|| {
664 ElabError::Frontend(format!(
665 "continuous assign `{}`'s assignment expression has no left()",
666 member.hierarchical_path()
667 ))
668 })?;
669 // A select / concatenation target (`assign w[3:0] = ..`,
670 // `assign {hi, lo} = ..`) drives each of its root symbols: one
671 // template per root, sharing the assign's read-set.
672 let target_symbols = lvalue_roots(lhs);
673 if target_symbols.is_empty() {
674 return Err(ElabError::Frontend(format!(
675 "continuous assign `{}`'s target expression names no variable or net",
676 member.hierarchical_path()
677 )));
678 }
679 let rhs = assignment.right().ok_or_else(|| {
680 ElabError::Frontend(format!(
681 "continuous assign `{}`'s assignment expression has no right()",
682 member.hierarchical_path()
683 ))
684 })?;
685
686 // (Load-bearing finding, TDD'd against real `sv-lang` dataflow
687 // output: `run_dataflow` over a `ContinuousAssign` symbol delivers
688 // a `DfaEventKind::Read` for the assignment's OWN LHS target
689 // BEFORE its `Write` — an artifact of evaluating the target as an
690 // lvalue address, not a genuine RHS dependency; e.g. for `assign y
691 // = a & b;` the raw event stream is `Read(y), Read(a), Read(b),
692 // Write(y)`.) `cont_assign_read_set` excludes exactly that artifact
693 // — the `target` read whose event node falls OUTSIDE the RHS's own
694 // source span — while keeping any OTHER read of `target` (a genuine
695 // RHS self-reference, e.g. `assign y = y | a;`, a legal LRM
696 // continuous-assign feedback/latch idiom). A blanket "drop every
697 // `target` entry" would silently lose that real dependency, since
698 // `ReadSetLattice` used to collapse both to the same
699 // `HashSet<SymbolId>` slot.
700 for target_symbol in target_symbols {
701 // A target in another instance (`assign g.w = ..` through an
702 // interface port, or a hierarchical name) is not a driver of
703 // this scope; the run pipeline lowers it like any other assign.
704 let Some(&target) = scope_keys.get(&target_symbol.uid()) else {
705 continue;
706 };
707 let read_set = if crate::netlist::exprwalk::has_pattern_conditional(rhs) {
708 let mut keys = crate::netlist::exprwalk::pattern_rhs_read_set(rhs, scope_keys)
709 .map_err(ElabError::Frontend)?;
710 keys.sort();
711 keys.dedup();
712 keys
713 } else if crate::netlist::exprwalk::has_logical_not(rhs) {
714 crate::netlist::exprwalk::rhs_tree_read_set(rhs, scope_keys)
715 } else {
716 cont_assign_read_set(
717 eval,
718 member,
719 target_symbol.uid(),
720 rhs.source_range(),
721 scope_keys,
722 )?
723 };
724 out.push(ContAssignTemplate {
725 target,
726 read_set,
727 is_output_port_connection: false,
728 });
729 }
730 }
731 Ok(out)
732}
733
734/// The variables/nets an assignment target expression writes: `w` for
735/// `w[3:0]` or `s.f`, both `a` and `b` for `{a, b}`.
736fn lvalue_roots<'d>(e: sv_lang::Expression<'d>) -> Vec<sv_lang::Symbol<'d>> {
737 use sv_lang_kinds::ExpressionKind;
738 let kind = e.kind();
739 if crate::symid::is_value_name(kind) {
740 e.referenced_symbol().into_iter().collect()
741 } else if matches!(
742 kind,
743 ExpressionKind::ElementSelect | ExpressionKind::RangeSelect
744 ) {
745 e.select_value().map(lvalue_roots).unwrap_or_default()
746 } else if kind == ExpressionKind::MemberAccess {
747 e.operand().map(lvalue_roots).unwrap_or_default()
748 } else if kind == ExpressionKind::Concatenation {
749 e.operands().into_iter().flat_map(lvalue_roots).collect()
750 } else {
751 Vec::new()
752 }
753}
754
755/// Visits one `Instance` symbol: allocates its (this-task-trivial) 1:1
756/// `Template`, records its `InstanceNode` (with a cached `hierarchical_path`
757/// — NL-021), then recurses into its body's child instances in declaration
758/// order (NL-010) before returning. Every instance — including every one
759/// found deep in the hierarchy — is visited exactly once; nothing is
760/// flattened into its parent's scope (NL-002).
761fn visit_instance<'d>(
762 netlist: &mut Netlist<'d>,
763 staged: &mut Vec<(sv_lang::Symbol<'d>, sv_lang::Symbol<'d>)>,
764 instance: sv_lang::Symbol<'d>,
765 parent: Option<InstanceId>,
766) -> Result<InstanceId, ElabError> {
767 let body = instance.instance_body().ok_or_else(|| {
768 ElabError::Frontend(format!(
769 "instance `{}` (kind Instance) has no instance_body()",
770 instance.hierarchical_path()
771 ))
772 })?;
773
774 // Cached once, here, per NL-021 — never recomputed by
775 // `NetlistView::hierarchical_path`.
776 let hier_path = instance.hierarchical_path();
777 let name = instance.name().to_string();
778 // NL-023: computed once, here, from the real `sv_lang::Symbol` handle —
779 // never re-derived from `InstanceNode` data after the fact.
780 let array_path = array_path_of(instance)?;
781
782 // Placeholder `TemplateId` (NL-014: every instance is visited and
783 // recorded — `staged` below — before Task 2's dedup pass ever runs;
784 // `dedup_templates` re-points this at the real, deduped `TemplateId`
785 // once the whole tree has been walked). `u32::MAX` rather than `0` so a
786 // bug that forgot to re-point a node is loudly wrong (out-of-range),
787 // not silently "instance 0's template" by coincidence.
788 let placeholder_template = TemplateId(u32::MAX);
789
790 let own_index = netlist.instances.push(InstanceNode {
791 id: InstanceId(0), // stamped with the real id immediately below
792 template: placeholder_template,
793 parent,
794 name,
795 array_path,
796 children: Vec::new(),
797 });
798 let id = InstanceId(own_index as u32);
799 netlist.instances.node_mut(id).id = id;
800 netlist.hier_paths.push(hier_path);
801 debug_assert_eq!(
802 staged.len(),
803 own_index,
804 "staged must stay index-aligned with InstanceId.0"
805 );
806 staged.push((instance, body));
807 debug_assert_eq!(
808 netlist.instance_syms.len(),
809 own_index,
810 "instance_syms must stay index-aligned with InstanceId.0"
811 );
812 netlist.instance_syms.push(instance);
813
814 let child_syms = collect_instance_children(body)?;
815 let mut child_ids = Vec::with_capacity(child_syms.len());
816 for child_sym in child_syms {
817 let child_id = visit_instance(netlist, staged, child_sym, Some(id))?;
818 child_ids.push(child_id);
819 }
820 netlist.instances.node_mut(id).children = child_ids;
821
822 Ok(id)
823}
824
825/// Walks one instance body's members (transitively through any enclosing
826/// `generate for`/instance-array constructs — NL-023) and returns the
827/// `Instance`-kind children (in declaration/iteration order — NL-010),
828/// rejecting any member kind this v1 elaboration slice cannot account for.
829///
830/// This match is intentionally *not* a blanket "skip anything that isn't an
831/// `Instance`": `SymbolKind` is `#[non_exhaustive]`, so every arm not
832/// explicitly listed falls to the wildcard, which returns a typed
833/// `ElabError::UnsupportedConstruct` rather than silently treating an
834/// unrecognized member as "not an instance, therefore irrelevant" — a
835/// construct this pass cannot classify might well be hiding a child
836/// instance, and silently skipping it would violate NL-002's "preserve full
837/// hierarchy" guarantee. Checkers and primitive instances are still
838/// deliberately routed to the error arm (out of this wave's scope); loop-
839/// generate blocks and instance arrays (Task 4) are now recognized and
840/// recursed into, so the real `Instance` symbols they contain are found and
841/// `array_path_of` (called back in `visit_instance`) can derive each one's
842/// `array_path` from its true generate-loop/array position.
843fn collect_instance_children<'d>(
844 body: sv_lang::Symbol<'d>,
845) -> Result<Vec<sv_lang::Symbol<'d>>, ElabError> {
846 let mut children = Vec::new();
847 collect_instance_children_into(body, &mut children)?;
848 Ok(children)
849}
850
851/// Recursive helper for [`collect_instance_children`]: appends every
852/// `Instance`-kind symbol transitively reachable from `scope`'s own
853/// `.members()`, descending into `GenerateBlockArray`/`GenerateBlock`/
854/// `InstanceArray` members (never flattening them into `Instance` nodes of
855/// their own — only real `Instance` symbols become `InstanceNode`s).
856fn collect_instance_children_into<'d>(
857 scope: sv_lang::Symbol<'d>,
858 children: &mut Vec<sv_lang::Symbol<'d>>,
859) -> Result<(), ElabError> {
860 use sv_lang_kinds::SymbolKind;
861
862 for member in scope.members() {
863 match member.kind() {
864 SymbolKind::Instance => children.push(member),
865 // A true instance-array (`modname name[hi:lo](...)`): descend
866 // into its elements, each itself `Instance` (or, for a
867 // multi-dimensional array, a nested `InstanceArray`).
868 SymbolKind::InstanceArray => collect_instance_array_elements(member, children)?,
869 // A `generate for`'s own array-of-blocks symbol: descend into
870 // each iteration's `GenerateBlock` entry, which is itself a
871 // scope containing that iteration's own declarations.
872 SymbolKind::GenerateBlockArray => {
873 for entry in member.generate_block_array_entries() {
874 collect_instance_children_into(entry, children)?;
875 }
876 }
877 // A non-loop generate block (`if`/`case`-generate branch) is
878 // also a plain scope — descend the same way, UNLESS this is the
879 // untaken branch of the `if`/`case` (Task 10, NL-116's own
880 // generate-if fixture surfaced this: `Symbol::
881 // generate_block_is_uninstantiated()` is `true` for a branch that
882 // was never actually elaborated, kept around only so name lookup
883 // inside it still type-checks — its own members are
884 // `UninstantiatedDef` placeholders, never real `Instance`
885 // symbols, verified empirically against pinned `sv-lang =
886 // "=0.1.9"` this task: `if (SEL) begin : g_true leaf u(..); end
887 // else begin : g_false leaf u(..); end` instantiated with
888 // `SEL=1` gives `g_false.generate_block_is_uninstantiated() ==
889 // true` and `g_false`'s own `u` member has `kind() ==
890 // SymbolKind::UninstantiatedDef`, not `Instance`). Descending
891 // into it anyway is wrong twice over: it is not part of the
892 // elaborated design at all (NL-002 only promises the REAL
893 // elaborated hierarchy), and its placeholder members are not
894 // recognized by this match, so descending used to surface as a
895 // spurious `ElabError::UnsupportedConstruct` on ordinary,
896 // LRM-legal `generate if`/`case` source.
897 SymbolKind::GenerateBlock if member.generate_block_is_uninstantiated() => {}
898 SymbolKind::GenerateBlock => collect_instance_children_into(member, children)?,
899 // Known-benign, non-instantiating members ordinary module
900 // bodies contain — explicitly enumerated (never a blanket `_`)
901 // so a genuinely unrecognized kind still falls to the error arm
902 // below.
903 SymbolKind::Port
904 | SymbolKind::MultiPort
905 | SymbolKind::InterfacePort
906 | SymbolKind::Modport
907 | SymbolKind::LetDecl
908 // An interface `extern task/function` prototype (25.7.1): a
909 // declaration, no storage; its exported implementations are
910 // resolved at call sites (`crate::netlist::itf`).
911 | SymbolKind::MethodPrototype
912 | SymbolKind::PrimitiveInstance
913 | SymbolKind::NetAlias
914 | SymbolKind::ClockingBlock
915 | SymbolKind::Net
916 | SymbolKind::Variable
917 | SymbolKind::ContinuousAssign
918 | SymbolKind::Parameter
919 | SymbolKind::TypeParameter
920 | SymbolKind::Genvar
921 | SymbolKind::ProceduralBlock
922 | SymbolKind::StatementBlock
923 | SymbolKind::Subroutine
924 // Named sequence/property declarations (16.8, 16.12) hold no
925 // storage and no instances; the assertions that use them are
926 // `ProceduralBlock`s (plan T29).
927 | SymbolKind::Sequence
928 | SymbolKind::Property
929 | SymbolKind::ElabSystemTask
930 | SymbolKind::TypeAlias
931 // A user-defined nettype declaration (LRM 6.6.7): no storage.
932 | SymbolKind::NetType
933 | SymbolKind::DefParam
934 | SymbolKind::Specparam
935 // A specify block (`docs/superpowers/plans/2026-10-03-sdf.md`
936 // Task 4a) never contains an `Instance` child to collect — its
937 // own members (`path_declaration`/`system_timing_check`/
938 // `pulsestyle_declaration`/`specparam_declaration`, LRM 30.3's
939 // own `specify_item` production) are discovered separately by
940 // `crate::netlist::specify::TimingModel::discover`, which
941 // itself does not recurse into a NESTED instance (30.3 gives a
942 // specify block no such production). Verified live: before
943 // this arm existed, ANY design containing `specify ...
944 // endspecify` failed HERE, in `run_design`'s very first
945 // elaboration pass, before `crate::specify`'s own discovery
946 // code ever ran — a real, blocking gap this plan's own "What
947 // already exists" survey did not catch.
948 | SymbolKind::SpecifyBlock
949 // A covergroup declaration holds no instance (its kernels are
950 // compiled per instance by `vj-codegen`'s `covergroup`).
951 | SymbolKind::CovergroupType
952 // A class declared in a module (8.1): its objects live in the
953 // object heap, never as instances or module storage.
954 | SymbolKind::ClassType
955 | SymbolKind::GenericClassDef
956 | SymbolKind::ForwardingTypedef
957 | SymbolKind::EmptyMember
958 | SymbolKind::TransparentMember
959 | SymbolKind::DeferredMember
960 | SymbolKind::Attribute
961 | SymbolKind::WildcardImport
962 | SymbolKind::ExplicitImport => {}
963 // A checker declaration holds no storage and no instances; its
964 // instances are lowered below.
965 SymbolKind::Checker => {}
966 // A checker instance is a pseudo-instance (17.3): its items
967 // run in the enclosing instance (`module_items`).
968 // Modules, interfaces and programs cannot be instantiated in a
969 // checker (17.2), so only nested checker instances matter.
970 SymbolKind::CheckerInstance => collect_instance_children_into(member, children)?,
971 // A checker's formal arguments (the inputs are substituted into
972 // the body; the outputs are variables of the checker).
973 SymbolKind::AssertionPort | SymbolKind::FormalArgument => {}
974 other => {
975 return Err(ElabError::UnsupportedConstruct {
976 hier_path: member.hierarchical_path(),
977 detail: format!("{other:?}"),
978 });
979 }
980 }
981 }
982 Ok(())
983}
984
985/// Recursive helper for the `InstanceArray` arm above: an array element is
986/// either a real `Instance` (the common case) or, for a multi-dimensional
987/// array (`modname name[1:0][1:0](...)`), a nested `InstanceArray` — any
988/// other element kind is a typed error, never a silent skip.
989fn collect_instance_array_elements<'d>(
990 array: sv_lang::Symbol<'d>,
991 children: &mut Vec<sv_lang::Symbol<'d>>,
992) -> Result<(), ElabError> {
993 use sv_lang_kinds::SymbolKind;
994
995 for element in array.instance_array_elements() {
996 match element.kind() {
997 SymbolKind::Instance => children.push(element),
998 SymbolKind::InstanceArray => collect_instance_array_elements(element, children)?,
999 // A primitive array's elements are primitives, not instances
1000 // (`vj`'s `primitives` realizes them).
1001 SymbolKind::PrimitiveInstance => {}
1002 // An array of checker instances (17.3): each element is a
1003 // pseudo-instance like a lone one (`module_items`).
1004 SymbolKind::CheckerInstance => collect_instance_children_into(element, children)?,
1005 other => {
1006 return Err(ElabError::UnsupportedConstruct {
1007 hier_path: element.hierarchical_path(),
1008 detail: format!("instance-array element of kind {other:?}"),
1009 });
1010 }
1011 }
1012 }
1013 Ok(())
1014}
1015
1016impl<'d> Netlist<'d> {
1017 /// Task 3 (Deviation #18): the real `sv_lang::Symbol<'d>` handle for one
1018 /// instance's own copy of a deduped `Template` member, keyed by that
1019 /// member's `SymbolKey`. `sv_lang::Symbol` is an internal handle type no
1020 /// external `NetlistView` consumer needs (the trait's public surface
1021 /// stays exactly `InstanceId`/`TemplateId`/`SymbolKey`-based), so the
1022 /// plan's own doc comment calls this "crate-private". Deviation from the
1023 /// plan's literal `pub(crate)`: verified this function must actually be
1024 /// `pub`, not `pub(crate)` — the plan's own test
1025 /// `member_symbols_are_distinct_real_symbols_per_instance_not_the_canonical_aliased`
1026 /// calls `netlist.member_symbol(..)` from `tests/netlist_dedup_invariants.rs`,
1027 /// an external integration-test crate (`tests/*.rs` files are compiled as
1028 /// separate crates and only see this crate's `pub` surface — exactly the
1029 /// reasoning the plan's own `confirm_and_assign_symbol_keys` doc comment
1030 /// gives for why *that* function is `pub`, immediately above this one).
1031 /// `pub(crate)` would make that test fail to compile. Still not part of
1032 /// `NetlistView` and not re-exported from `netlist::mod` beyond
1033 /// `Netlist` itself, so it remains effectively internal to callers that
1034 /// hold a concrete `Netlist<'d>` (same crate, or an integration test).
1035 /// Panics only if called with an `(instance, key)` pair `elaborate()`
1036 /// itself never produced — every real caller (Task 8, same crate) only
1037 /// ever passes pairs read off this same `Netlist`'s own
1038 /// `Template`/`InstanceTree` data, so this can never fire for a correct
1039 /// caller; a wrong caller is a programmer error this wave chooses to
1040 /// catch loudly, not a `Result` (this is not an `sv_lang`/`sv_lang_kinds`
1041 /// kind match, so it is not bound by this plan's "match must fall
1042 /// through to a typed error" rule — it is an internal indexing
1043 /// precondition).
1044 pub fn member_symbol(&self, instance: InstanceId, key: SymbolKey) -> sv_lang::Symbol<'d> {
1045 self.member_symbols[instance.0 as usize][key.0 as usize]
1046 }
1047
1048 /// Task F3 (CG-125/NL-035): the real, undeduped `Instance`-kind
1049 /// `sv_lang::Symbol<'d>` for `id` — the instantiation site itself (see
1050 /// `instance_syms`'s own doc comment for why this, not `member_symbol`,
1051 /// is what the run pipeline needs to reach `instance_body()`/
1052 /// `instance_port_connections()`). Panics only for an `InstanceId`
1053 /// `elaborate()` itself never produced, the same indexing-precondition
1054 /// posture `member_symbol`'s own doc comment documents immediately
1055 /// above.
1056 pub fn instance_symbol(&self, id: InstanceId) -> sv_lang::Symbol<'d> {
1057 self.instance_syms[id.0 as usize]
1058 }
1059}
1060
1061impl<'d> NetlistView for Netlist<'d> {
1062 fn registration_order(&self) -> Vec<InstanceId> {
1063 // The `InstanceTree` is populated in exactly this order by
1064 // `visit_instance`'s DFS above (parent pushed before any of its
1065 // children) — no separate sort pass exists.
1066 (0..self.instances.len() as u32).map(InstanceId).collect()
1067 }
1068
1069 fn hierarchical_path(&self, id: InstanceId) -> &str {
1070 &self.hier_paths[id.0 as usize]
1071 }
1072
1073 fn resolve_hier_path(&self, path: &str) -> Option<InstantiatedSignalId> {
1074 // Task 5: a real per-level walk through `InstanceTree`, never a
1075 // global name->id map (so two same-named children under different
1076 // parents can never collide — see this task's own
1077 // `resolve_hier_path_finds_the_correct_signal_on_the_correct_sibling_instance`
1078 // test). `path` needs at least a top-instance segment and a final
1079 // signal segment (`"top.sig"`); anything shorter has no signal to
1080 // resolve to.
1081 let segments: Vec<&str> = path.split('.').collect();
1082 let (first, rest) = segments.split_first()?;
1083 let (last, middle) = rest.split_last()?;
1084
1085 let mut current_id = *self
1086 .top
1087 .iter()
1088 .find(|&&id| self.instances.node(id).name == *first)?;
1089
1090 for seg in middle {
1091 current_id = *self
1092 .instances
1093 .node(current_id)
1094 .children
1095 .iter()
1096 .find(|&&child_id| self.instances.node(child_id).name == *seg)?;
1097 }
1098
1099 // Ports are intentionally not also searched here: every port already
1100 // has a same-named internal driving/driven net or variable in this
1101 // pass's lowering (per NL-034/NL-035's synthetic-ContAssign
1102 // convention) — searching `signals` alone is sufficient and avoids a
1103 // double-match against two different `SymbolKey`s for what a caller
1104 // would call "the same" hierarchical name.
1105 let template_id = self.instances.node(current_id).template;
1106 let signal = self.templates[template_id.0 as usize]
1107 .signals
1108 .iter()
1109 .find(|signal| signal.name == *last)?;
1110
1111 Some(InstantiatedSignalId {
1112 instance: current_id,
1113 member: signal.key,
1114 })
1115 }
1116
1117 fn dedup_key(&self, tmpl: TemplateId) -> &TemplateKey {
1118 &self.templates[tmpl.0 as usize].key
1119 }
1120
1121 fn tick_ns_per_unit(&self) -> u64 {
1122 // Task 6 (NL-025): the real field, computed once in `elaborate()` —
1123 // see `tick_ns_per_unit_of`'s doc comment.
1124 self.tick_ns_per_unit
1125 }
1126
1127 fn instance(&self, id: InstanceId) -> &InstanceNode {
1128 self.instances.node(id)
1129 }
1130
1131 fn template(&self, id: TemplateId) -> &Template {
1132 &self.templates[id.0 as usize]
1133 }
1134}