Skip to main content

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}