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 {
714 cont_assign_read_set(
715 eval,
716 member,
717 target_symbol.uid(),
718 rhs.source_range(),
719 scope_keys,
720 )?
721 };
722 out.push(ContAssignTemplate {
723 target,
724 read_set,
725 is_output_port_connection: false,
726 });
727 }
728 }
729 Ok(out)
730}
731
732/// The variables/nets an assignment target expression writes: `w` for
733/// `w[3:0]` or `s.f`, both `a` and `b` for `{a, b}`.
734fn lvalue_roots<'d>(e: sv_lang::Expression<'d>) -> Vec<sv_lang::Symbol<'d>> {
735 use sv_lang_kinds::ExpressionKind;
736 let kind = e.kind();
737 if crate::symid::is_value_name(kind) {
738 e.referenced_symbol().into_iter().collect()
739 } else if matches!(
740 kind,
741 ExpressionKind::ElementSelect | ExpressionKind::RangeSelect
742 ) {
743 e.select_value().map(lvalue_roots).unwrap_or_default()
744 } else if kind == ExpressionKind::MemberAccess {
745 e.operand().map(lvalue_roots).unwrap_or_default()
746 } else if kind == ExpressionKind::Concatenation {
747 e.operands().into_iter().flat_map(lvalue_roots).collect()
748 } else {
749 Vec::new()
750 }
751}
752
753/// Visits one `Instance` symbol: allocates its (this-task-trivial) 1:1
754/// `Template`, records its `InstanceNode` (with a cached `hierarchical_path`
755/// — NL-021), then recurses into its body's child instances in declaration
756/// order (NL-010) before returning. Every instance — including every one
757/// found deep in the hierarchy — is visited exactly once; nothing is
758/// flattened into its parent's scope (NL-002).
759fn visit_instance<'d>(
760 netlist: &mut Netlist<'d>,
761 staged: &mut Vec<(sv_lang::Symbol<'d>, sv_lang::Symbol<'d>)>,
762 instance: sv_lang::Symbol<'d>,
763 parent: Option<InstanceId>,
764) -> Result<InstanceId, ElabError> {
765 let body = instance.instance_body().ok_or_else(|| {
766 ElabError::Frontend(format!(
767 "instance `{}` (kind Instance) has no instance_body()",
768 instance.hierarchical_path()
769 ))
770 })?;
771
772 // Cached once, here, per NL-021 — never recomputed by
773 // `NetlistView::hierarchical_path`.
774 let hier_path = instance.hierarchical_path();
775 let name = instance.name().to_string();
776 // NL-023: computed once, here, from the real `sv_lang::Symbol` handle —
777 // never re-derived from `InstanceNode` data after the fact.
778 let array_path = array_path_of(instance)?;
779
780 // Placeholder `TemplateId` (NL-014: every instance is visited and
781 // recorded — `staged` below — before Task 2's dedup pass ever runs;
782 // `dedup_templates` re-points this at the real, deduped `TemplateId`
783 // once the whole tree has been walked). `u32::MAX` rather than `0` so a
784 // bug that forgot to re-point a node is loudly wrong (out-of-range),
785 // not silently "instance 0's template" by coincidence.
786 let placeholder_template = TemplateId(u32::MAX);
787
788 let own_index = netlist.instances.push(InstanceNode {
789 id: InstanceId(0), // stamped with the real id immediately below
790 template: placeholder_template,
791 parent,
792 name,
793 array_path,
794 children: Vec::new(),
795 });
796 let id = InstanceId(own_index as u32);
797 netlist.instances.node_mut(id).id = id;
798 netlist.hier_paths.push(hier_path);
799 debug_assert_eq!(
800 staged.len(),
801 own_index,
802 "staged must stay index-aligned with InstanceId.0"
803 );
804 staged.push((instance, body));
805 debug_assert_eq!(
806 netlist.instance_syms.len(),
807 own_index,
808 "instance_syms must stay index-aligned with InstanceId.0"
809 );
810 netlist.instance_syms.push(instance);
811
812 let child_syms = collect_instance_children(body)?;
813 let mut child_ids = Vec::with_capacity(child_syms.len());
814 for child_sym in child_syms {
815 let child_id = visit_instance(netlist, staged, child_sym, Some(id))?;
816 child_ids.push(child_id);
817 }
818 netlist.instances.node_mut(id).children = child_ids;
819
820 Ok(id)
821}
822
823/// Walks one instance body's members (transitively through any enclosing
824/// `generate for`/instance-array constructs — NL-023) and returns the
825/// `Instance`-kind children (in declaration/iteration order — NL-010),
826/// rejecting any member kind this v1 elaboration slice cannot account for.
827///
828/// This match is intentionally *not* a blanket "skip anything that isn't an
829/// `Instance`": `SymbolKind` is `#[non_exhaustive]`, so every arm not
830/// explicitly listed falls to the wildcard, which returns a typed
831/// `ElabError::UnsupportedConstruct` rather than silently treating an
832/// unrecognized member as "not an instance, therefore irrelevant" — a
833/// construct this pass cannot classify might well be hiding a child
834/// instance, and silently skipping it would violate NL-002's "preserve full
835/// hierarchy" guarantee. Checkers and primitive instances are still
836/// deliberately routed to the error arm (out of this wave's scope); loop-
837/// generate blocks and instance arrays (Task 4) are now recognized and
838/// recursed into, so the real `Instance` symbols they contain are found and
839/// `array_path_of` (called back in `visit_instance`) can derive each one's
840/// `array_path` from its true generate-loop/array position.
841fn collect_instance_children<'d>(
842 body: sv_lang::Symbol<'d>,
843) -> Result<Vec<sv_lang::Symbol<'d>>, ElabError> {
844 let mut children = Vec::new();
845 collect_instance_children_into(body, &mut children)?;
846 Ok(children)
847}
848
849/// Recursive helper for [`collect_instance_children`]: appends every
850/// `Instance`-kind symbol transitively reachable from `scope`'s own
851/// `.members()`, descending into `GenerateBlockArray`/`GenerateBlock`/
852/// `InstanceArray` members (never flattening them into `Instance` nodes of
853/// their own — only real `Instance` symbols become `InstanceNode`s).
854fn collect_instance_children_into<'d>(
855 scope: sv_lang::Symbol<'d>,
856 children: &mut Vec<sv_lang::Symbol<'d>>,
857) -> Result<(), ElabError> {
858 use sv_lang_kinds::SymbolKind;
859
860 for member in scope.members() {
861 match member.kind() {
862 SymbolKind::Instance => children.push(member),
863 // A true instance-array (`modname name[hi:lo](...)`): descend
864 // into its elements, each itself `Instance` (or, for a
865 // multi-dimensional array, a nested `InstanceArray`).
866 SymbolKind::InstanceArray => collect_instance_array_elements(member, children)?,
867 // A `generate for`'s own array-of-blocks symbol: descend into
868 // each iteration's `GenerateBlock` entry, which is itself a
869 // scope containing that iteration's own declarations.
870 SymbolKind::GenerateBlockArray => {
871 for entry in member.generate_block_array_entries() {
872 collect_instance_children_into(entry, children)?;
873 }
874 }
875 // A non-loop generate block (`if`/`case`-generate branch) is
876 // also a plain scope — descend the same way, UNLESS this is the
877 // untaken branch of the `if`/`case` (Task 10, NL-116's own
878 // generate-if fixture surfaced this: `Symbol::
879 // generate_block_is_uninstantiated()` is `true` for a branch that
880 // was never actually elaborated, kept around only so name lookup
881 // inside it still type-checks — its own members are
882 // `UninstantiatedDef` placeholders, never real `Instance`
883 // symbols, verified empirically against pinned `sv-lang =
884 // "=0.1.9"` this task: `if (SEL) begin : g_true leaf u(..); end
885 // else begin : g_false leaf u(..); end` instantiated with
886 // `SEL=1` gives `g_false.generate_block_is_uninstantiated() ==
887 // true` and `g_false`'s own `u` member has `kind() ==
888 // SymbolKind::UninstantiatedDef`, not `Instance`). Descending
889 // into it anyway is wrong twice over: it is not part of the
890 // elaborated design at all (NL-002 only promises the REAL
891 // elaborated hierarchy), and its placeholder members are not
892 // recognized by this match, so descending used to surface as a
893 // spurious `ElabError::UnsupportedConstruct` on ordinary,
894 // LRM-legal `generate if`/`case` source.
895 SymbolKind::GenerateBlock if member.generate_block_is_uninstantiated() => {}
896 SymbolKind::GenerateBlock => collect_instance_children_into(member, children)?,
897 // Known-benign, non-instantiating members ordinary module
898 // bodies contain — explicitly enumerated (never a blanket `_`)
899 // so a genuinely unrecognized kind still falls to the error arm
900 // below.
901 SymbolKind::Port
902 | SymbolKind::MultiPort
903 | SymbolKind::InterfacePort
904 | SymbolKind::Modport
905 | SymbolKind::LetDecl
906 // An interface `extern task/function` prototype (25.7.1): a
907 // declaration, no storage; its exported implementations are
908 // resolved at call sites (`crate::netlist::itf`).
909 | SymbolKind::MethodPrototype
910 | SymbolKind::PrimitiveInstance
911 | SymbolKind::NetAlias
912 | SymbolKind::ClockingBlock
913 | SymbolKind::Net
914 | SymbolKind::Variable
915 | SymbolKind::ContinuousAssign
916 | SymbolKind::Parameter
917 | SymbolKind::TypeParameter
918 | SymbolKind::Genvar
919 | SymbolKind::ProceduralBlock
920 | SymbolKind::StatementBlock
921 | SymbolKind::Subroutine
922 // Named sequence/property declarations (16.8, 16.12) hold no
923 // storage and no instances; the assertions that use them are
924 // `ProceduralBlock`s (plan T29).
925 | SymbolKind::Sequence
926 | SymbolKind::Property
927 | SymbolKind::ElabSystemTask
928 | SymbolKind::TypeAlias
929 // A user-defined nettype declaration (LRM 6.6.7): no storage.
930 | SymbolKind::NetType
931 | SymbolKind::DefParam
932 | SymbolKind::Specparam
933 // A specify block (`docs/superpowers/plans/2026-10-03-sdf.md`
934 // Task 4a) never contains an `Instance` child to collect — its
935 // own members (`path_declaration`/`system_timing_check`/
936 // `pulsestyle_declaration`/`specparam_declaration`, LRM 30.3's
937 // own `specify_item` production) are discovered separately by
938 // `crate::netlist::specify::TimingModel::discover`, which
939 // itself does not recurse into a NESTED instance (30.3 gives a
940 // specify block no such production). Verified live: before
941 // this arm existed, ANY design containing `specify ...
942 // endspecify` failed HERE, in `run_design`'s very first
943 // elaboration pass, before `crate::specify`'s own discovery
944 // code ever ran — a real, blocking gap this plan's own "What
945 // already exists" survey did not catch.
946 | SymbolKind::SpecifyBlock
947 // A covergroup declaration holds no instance (its kernels are
948 // compiled per instance by `vj-codegen`'s `covergroup`).
949 | SymbolKind::CovergroupType
950 // A class declared in a module (8.1): its objects live in the
951 // object heap, never as instances or module storage.
952 | SymbolKind::ClassType
953 | SymbolKind::GenericClassDef
954 | SymbolKind::ForwardingTypedef
955 | SymbolKind::EmptyMember
956 | SymbolKind::TransparentMember
957 | SymbolKind::DeferredMember
958 | SymbolKind::Attribute
959 | SymbolKind::WildcardImport
960 | SymbolKind::ExplicitImport => {}
961 // A checker declaration holds no storage and no instances; its
962 // instances are lowered below.
963 SymbolKind::Checker => {}
964 // A checker instance is a pseudo-instance (17.3): its items
965 // run in the enclosing instance (`module_items`).
966 // Modules, interfaces and programs cannot be instantiated in a
967 // checker (17.2), so only nested checker instances matter.
968 SymbolKind::CheckerInstance => collect_instance_children_into(member, children)?,
969 // A checker's formal arguments (the inputs are substituted into
970 // the body; the outputs are variables of the checker).
971 SymbolKind::AssertionPort | SymbolKind::FormalArgument => {}
972 other => {
973 return Err(ElabError::UnsupportedConstruct {
974 hier_path: member.hierarchical_path(),
975 detail: format!("{other:?}"),
976 });
977 }
978 }
979 }
980 Ok(())
981}
982
983/// Recursive helper for the `InstanceArray` arm above: an array element is
984/// either a real `Instance` (the common case) or, for a multi-dimensional
985/// array (`modname name[1:0][1:0](...)`), a nested `InstanceArray` — any
986/// other element kind is a typed error, never a silent skip.
987fn collect_instance_array_elements<'d>(
988 array: sv_lang::Symbol<'d>,
989 children: &mut Vec<sv_lang::Symbol<'d>>,
990) -> Result<(), ElabError> {
991 use sv_lang_kinds::SymbolKind;
992
993 for element in array.instance_array_elements() {
994 match element.kind() {
995 SymbolKind::Instance => children.push(element),
996 SymbolKind::InstanceArray => collect_instance_array_elements(element, children)?,
997 // A primitive array's elements are primitives, not instances
998 // (`vj`'s `primitives` realizes them).
999 SymbolKind::PrimitiveInstance => {}
1000 // An array of checker instances (17.3): each element is a
1001 // pseudo-instance like a lone one (`module_items`).
1002 SymbolKind::CheckerInstance => collect_instance_children_into(element, children)?,
1003 other => {
1004 return Err(ElabError::UnsupportedConstruct {
1005 hier_path: element.hierarchical_path(),
1006 detail: format!("instance-array element of kind {other:?}"),
1007 });
1008 }
1009 }
1010 }
1011 Ok(())
1012}
1013
1014impl<'d> Netlist<'d> {
1015 /// Task 3 (Deviation #18): the real `sv_lang::Symbol<'d>` handle for one
1016 /// instance's own copy of a deduped `Template` member, keyed by that
1017 /// member's `SymbolKey`. `sv_lang::Symbol` is an internal handle type no
1018 /// external `NetlistView` consumer needs (the trait's public surface
1019 /// stays exactly `InstanceId`/`TemplateId`/`SymbolKey`-based), so the
1020 /// plan's own doc comment calls this "crate-private". Deviation from the
1021 /// plan's literal `pub(crate)`: verified this function must actually be
1022 /// `pub`, not `pub(crate)` — the plan's own test
1023 /// `member_symbols_are_distinct_real_symbols_per_instance_not_the_canonical_aliased`
1024 /// calls `netlist.member_symbol(..)` from `tests/netlist_dedup_invariants.rs`,
1025 /// an external integration-test crate (`tests/*.rs` files are compiled as
1026 /// separate crates and only see this crate's `pub` surface — exactly the
1027 /// reasoning the plan's own `confirm_and_assign_symbol_keys` doc comment
1028 /// gives for why *that* function is `pub`, immediately above this one).
1029 /// `pub(crate)` would make that test fail to compile. Still not part of
1030 /// `NetlistView` and not re-exported from `netlist::mod` beyond
1031 /// `Netlist` itself, so it remains effectively internal to callers that
1032 /// hold a concrete `Netlist<'d>` (same crate, or an integration test).
1033 /// Panics only if called with an `(instance, key)` pair `elaborate()`
1034 /// itself never produced — every real caller (Task 8, same crate) only
1035 /// ever passes pairs read off this same `Netlist`'s own
1036 /// `Template`/`InstanceTree` data, so this can never fire for a correct
1037 /// caller; a wrong caller is a programmer error this wave chooses to
1038 /// catch loudly, not a `Result` (this is not an `sv_lang`/`sv_lang_kinds`
1039 /// kind match, so it is not bound by this plan's "match must fall
1040 /// through to a typed error" rule — it is an internal indexing
1041 /// precondition).
1042 pub fn member_symbol(&self, instance: InstanceId, key: SymbolKey) -> sv_lang::Symbol<'d> {
1043 self.member_symbols[instance.0 as usize][key.0 as usize]
1044 }
1045
1046 /// Task F3 (CG-125/NL-035): the real, undeduped `Instance`-kind
1047 /// `sv_lang::Symbol<'d>` for `id` — the instantiation site itself (see
1048 /// `instance_syms`'s own doc comment for why this, not `member_symbol`,
1049 /// is what the run pipeline needs to reach `instance_body()`/
1050 /// `instance_port_connections()`). Panics only for an `InstanceId`
1051 /// `elaborate()` itself never produced, the same indexing-precondition
1052 /// posture `member_symbol`'s own doc comment documents immediately
1053 /// above.
1054 pub fn instance_symbol(&self, id: InstanceId) -> sv_lang::Symbol<'d> {
1055 self.instance_syms[id.0 as usize]
1056 }
1057}
1058
1059impl<'d> NetlistView for Netlist<'d> {
1060 fn registration_order(&self) -> Vec<InstanceId> {
1061 // The `InstanceTree` is populated in exactly this order by
1062 // `visit_instance`'s DFS above (parent pushed before any of its
1063 // children) — no separate sort pass exists.
1064 (0..self.instances.len() as u32).map(InstanceId).collect()
1065 }
1066
1067 fn hierarchical_path(&self, id: InstanceId) -> &str {
1068 &self.hier_paths[id.0 as usize]
1069 }
1070
1071 fn resolve_hier_path(&self, path: &str) -> Option<InstantiatedSignalId> {
1072 // Task 5: a real per-level walk through `InstanceTree`, never a
1073 // global name->id map (so two same-named children under different
1074 // parents can never collide — see this task's own
1075 // `resolve_hier_path_finds_the_correct_signal_on_the_correct_sibling_instance`
1076 // test). `path` needs at least a top-instance segment and a final
1077 // signal segment (`"top.sig"`); anything shorter has no signal to
1078 // resolve to.
1079 let segments: Vec<&str> = path.split('.').collect();
1080 let (first, rest) = segments.split_first()?;
1081 let (last, middle) = rest.split_last()?;
1082
1083 let mut current_id = *self
1084 .top
1085 .iter()
1086 .find(|&&id| self.instances.node(id).name == *first)?;
1087
1088 for seg in middle {
1089 current_id = *self
1090 .instances
1091 .node(current_id)
1092 .children
1093 .iter()
1094 .find(|&&child_id| self.instances.node(child_id).name == *seg)?;
1095 }
1096
1097 // Ports are intentionally not also searched here: every port already
1098 // has a same-named internal driving/driven net or variable in this
1099 // pass's lowering (per NL-034/NL-035's synthetic-ContAssign
1100 // convention) — searching `signals` alone is sufficient and avoids a
1101 // double-match against two different `SymbolKey`s for what a caller
1102 // would call "the same" hierarchical name.
1103 let template_id = self.instances.node(current_id).template;
1104 let signal = self.templates[template_id.0 as usize]
1105 .signals
1106 .iter()
1107 .find(|signal| signal.name == *last)?;
1108
1109 Some(InstantiatedSignalId {
1110 instance: current_id,
1111 member: signal.key,
1112 })
1113 }
1114
1115 fn dedup_key(&self, tmpl: TemplateId) -> &TemplateKey {
1116 &self.templates[tmpl.0 as usize].key
1117 }
1118
1119 fn tick_ns_per_unit(&self) -> u64 {
1120 // Task 6 (NL-025): the real field, computed once in `elaborate()` —
1121 // see `tick_ns_per_unit_of`'s doc comment.
1122 self.tick_ns_per_unit
1123 }
1124
1125 fn instance(&self, id: InstanceId) -> &InstanceNode {
1126 self.instances.node(id)
1127 }
1128
1129 fn template(&self, id: TemplateId) -> &Template {
1130 &self.templates[id.0 as usize]
1131 }
1132}