vj-sim 0.3.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
//! The deterministic DFS elaboration entry point (NL-001, NL-002, NL-008,
//! NL-010, NL-021). Frontend reuse only: this module consumes
//! `sv_lang::Design` (via `EvalSession::design()`) directly — there is no
//! parallel parse/elaborate path anywhere in this crate.
//!
//! Call order (binding for every caller, including this module's own tests
//! and doctest — see the plan's "Load-bearing lifetime discovery"):
//! ```
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use sv_lang::{Compilation, Session};
//! use vj_sim::netlist;
//!
//! let session = Session::new();
//! let mut comp = Compilation::new(&session)?;
//! comp.add_source("module m; endmodule\n")?;
//! let mut design = comp.compile()?;           // owned Design
//! let eval = design.eval_session();           // the ONE &mut borrow, taken first
//! let netlist = netlist::elaborate(&eval)?;    // elaborate() takes `eval` itself
//! assert_eq!(netlist.top.len(), 1);
//! # Ok(()) }
//! ```

use crate::symid::SymbolUid;
use std::collections::HashMap;

use crate::netlist::dedup::{DefinitionTable, template_key_of};
use crate::netlist::exprwalk::expression_read_set;
use crate::netlist::instance::array_path_of;
use crate::netlist::lattice::cont_assign_read_set;
use crate::netlist::template::{build_members, build_processes};
use crate::netlist::{
    ContAssignTemplate, ElabError, InstanceId, InstanceNode, InstanceTree, InstantiatedSignalId,
    NetlistView, PortDirection, SubInstanceSlot, SymbolKey, Template, TemplateId, TemplateKey,
    confirm_and_assign_symbol_keys, merge_groups_within_bucket,
};

/// The hand-off value `elaborate()` produces: the full undeduped
/// `InstanceTree` plus (this task) a trivial 1:1 `Template` per instance.
pub struct Netlist<'d> {
    pub templates: Vec<Template>,
    pub instances: InstanceTree,
    pub top: Vec<InstanceId>,
    /// `hierarchical_path()` cache (NL-021): computed exactly once per
    /// instance during the DFS below, index-aligned with `InstanceId.0`,
    /// never rebuilt on repeated `NetlistView::hierarchical_path` calls.
    hier_paths: Vec<String>,
    /// Declared here (per the plan's Task 1 spec) so `Netlist`'s shape is
    /// stable across tasks. Populated by `dedup_templates` (Task 3) — the one
    /// place a live per-instance, per-member `Symbol<'d>` is captured, for
    /// Task 8's driver-graph construction. Index-aligned with `InstanceId.0`
    /// (one entry per staged instance, whether or not it merged with
    /// another); each inner `Vec` is `SymbolKey`-ordered. Read only through
    /// `Netlist::member_symbol` below.
    member_symbols: Vec<Vec<sv_lang::Symbol<'d>>>,
    /// Task F3 (`docs/superpowers/plans/2026-09-27-wave-A2-slice-finish.md`,
    /// CG-125/NL-035): the real `Instance`-kind `sv_lang::Symbol<'d>` for
    /// each elaborated instance, index-aligned with `InstanceId.0` — pushed
    /// in `visit_instance` at the same point `staged`/`hier_paths` are
    /// pushed, never re-derived later. Unlike `member_symbols` (which only
    /// carries a *deduped Template member's* per-instance symbol, addressed
    /// by `SymbolKey`), this is the instantiation site itself: the one
    /// handle the run pipeline needs to walk `Symbol::instance_port_connections()`
    /// (each port's own formal `Symbol` plus its parent-scope binding
    /// `Expression`) and `Symbol::instance_body()` (that instance's own,
    /// undeduped procedural-block/signal members) to build per-instance
    /// state and synthetic port-connection processes. Exposed via
    /// [`Netlist::instance_symbol`].
    instance_syms: Vec<sv_lang::Symbol<'d>>,
    /// Task 6 (NL-026): the design's global time scale — see
    /// `resolve_time_scale`'s doc comment for exactly where this comes from
    /// (a documented deviation from the plan's literal spec).
    pub time_scale: sv_lang::TimeScale,
    /// Task 6 (NL-025): the tick-per-nanosecond normalization factor SCHED
    /// reads as its sole source of truth — see `tick_ns_per_unit_of`.
    pub tick_ns_per_unit: u64,
    _design: std::marker::PhantomData<&'d sv_lang::Design>,
}

/// Folds every unpacked-struct member's default value (`struct { bit [3:0]
/// lo = 5; } s;`, LRM 1800-2023 7.2.2) so the storage layout can read it:
/// slang prefolds variable initializers but leaves a member's bare literal
/// unfolded, and evaluating it caches the value on the node.
fn fold_struct_member_defaults(eval: &sv_lang::EvalSession<'_>) {
    fn sweep(ty: sv_lang::Type<'_>, eval: &sv_lang::EvalSession<'_>, depth: u32) {
        if depth > 8 || !ty.is_unpacked_struct() {
            return;
        }
        for f in ty.fields() {
            if let Some(init) = f.initializer() {
                let _ = eval.eval_constant(init);
            }
            if let Some(fty) = f.value_type() {
                sweep(fty, eval, depth + 1);
            }
        }
    }
    eval.design().root().visit(|s| {
        if let Some(t) = s.declared_type() {
            sweep(t.canonical(), eval, 0);
        }
        sv_lang::Walk::Continue
    });
}

/// Applies the IEEE 1800-2023 18.5.1 constraint-prototype checks to every
/// class declaration reachable from the root, including classes that are
/// never instantiated (an unused `extern constraint c;` is still an error).
fn check_declared_class_constraints(eval: &sv_lang::EvalSession<'_>) -> Result<(), ElabError> {
    let mut result = Ok(());
    eval.design().root().visit(|s| {
        if s.kind() == sv_lang_kinds::SymbolKind::ClassType
            && let Err(e) = super::classes::check_class_constraints(s)
        {
            result = Err(e);
            return sv_lang::Walk::Break;
        }
        sv_lang::Walk::Continue
    });
    result
}

/// Deterministic DFS over `Design::top_instances()` in declaration order —
/// this order **is** SCHED's `registration_order` (ARCHITECTURE §6.5; no
/// separate sort pass exists anywhere in this pipeline). Never panics on
/// malformed/unsupported-but-valid input: every failure path returns a typed
/// `ElabError` (NL-008).
///
/// Takes the `EvalSession` itself (not a bare `&'d Design`): later tasks in
/// this same pipeline (Task 7) need `EvalSession::run_dataflow` from inside
/// this same function, and `Design::eval_session` requires `&mut Design`, so
/// the one `&mut` borrow it performs must happen exactly once, before
/// `elaborate()` is ever called — see the module doc's call-order example.
///
/// Deviation from the plan's literal signature (`eval: &sv_lang::EvalSession<'d>`,
/// with the outer reference's own lifetime left elided): `EvalSession::design(&self)
/// -> &Design` ties its return value's lifetime to the `&self` borrow used for
/// *that* call, per ordinary Rust elision — not to `EvalSession`'s own `'d`
/// parameter. With the outer reference elided (effectively some anonymous,
/// caller-chosen, possibly-shorter lifetime), every `Symbol` obtained via
/// `eval.design()...` inside this function would carry that shorter lifetime,
/// not `'d`, and could never be returned inside `Netlist<'d>` (needed once
/// `member_symbols: Vec<Vec<Symbol<'d>>>` is actually populated, Task 3).
/// Pinning the outer reference to `'d` too (`&'d EvalSession<'d>`) makes
/// `eval.design()` reborrow at exactly `'d`, so every `Symbol` this function
/// obtains is genuinely `Symbol<'d>` — the plan's own intent, just spelled
/// out explicitly rather than relying on elision to reach it.
pub fn elaborate<'d>(eval: &'d sv_lang::EvalSession<'d>) -> Result<Netlist<'d>, ElabError> {
    // Task 6 (NL-025, NL-026): computed once, up front — independent of the
    // DFS/dedup state built below (both only read `eval.design().
    // top_instances()`, never anything `visit_instance`/`dedup_templates`
    // produce) — and stored directly into the `Netlist` literal so there is
    // no separate "placeholder then overwrite" step to forget.
    let time_scale = resolve_time_scale(eval);
    let tick_ns_per_unit = tick_ns_per_unit_of(time_scale.precision)?;
    fold_struct_member_defaults(eval);
    check_declared_class_constraints(eval)?;

    let mut netlist = Netlist {
        templates: Vec::new(),
        instances: InstanceTree::new(),
        top: Vec::new(),
        hier_paths: Vec::new(),
        member_symbols: Vec::new(),
        instance_syms: Vec::new(),
        time_scale,
        tick_ns_per_unit,
        _design: std::marker::PhantomData,
    };
    // Staging list of (Instance symbol, InstanceBody symbol), index-aligned
    // with `InstanceId.0` — every reachable instance body is visited and
    // recorded here BEFORE Task 2's dedup pass runs below (NL-014: walk
    // before collapse). Task 2's own bucketing needs both handles per
    // instance: `body` for `instance_body_definition()`/
    // `instance_body_has_same_type`, `instance` for `parameters()` (see
    // `dedup::template_key_of`'s doc comment for why `body` alone is not
    // enough).
    let mut staged: Vec<(sv_lang::Symbol<'d>, sv_lang::Symbol<'d>)> = Vec::new();

    for top_sym in eval.design().top_instances() {
        match top_sym.kind() {
            sv_lang_kinds::SymbolKind::Instance => {
                let id = visit_instance(&mut netlist, &mut staged, top_sym, None)?;
                netlist.top.push(id);
            }
            other => {
                // Defensive: `Design::top_instances()` is documented to
                // yield only module/program `Instance` symbols, but this
                // pass never trusts that silently — an unrecognized
                // top-level symbol kind is a typed error, not a panic or a
                // silently-dropped top module.
                return Err(ElabError::UnsupportedConstruct {
                    hier_path: top_sym.hierarchical_path(),
                    detail: format!(
                        "top-level symbol of kind {other:?} is not a module/program instance"
                    ),
                });
            }
        }
    }

    dedup_templates(&mut netlist, &staged, eval)?;

    Ok(netlist)
}

/// Task 7 (NL-034, NL-035): builds a `SymbolId -> SymbolKey` table for one
/// scope, keyed by `Symbol::id()` (the hierarchical-path-keyed, context-free
/// stable identity `sv-lang` itself publishes — see `lattice.rs`'s own doc
/// comment for why this indirection, rather than a `SymbolKey` inside the
/// `Lattice`/expression-walker themselves, is required). Position-based:
/// `scope.members().enumerate()`, the SAME flat walk `build_members`/
/// `confirm_and_assign_symbol_keys` already perform over the same canonical
/// body, so a key this table produces always equals the `SymbolKey` that
/// body's own `Template`/`SignalDecl`/`PortDecl`/`ProcessTemplate` entries
/// carry for the same logical member — never a second, independently
/// re-derived numbering. Valid for ANY instance body (not only a group's
/// canonical member): dedup's merge invariant (Task 3) guarantees every
/// body in a merge group enumerates its members in the identical
/// (kind, name) order, so the k-th member of ANY of them is "the same"
/// logical member as the k-th member of the canonical body, and gets the
/// same `SymbolKey`.
///
/// **Load-bearing finding, TDD'd against real `sv-lang` output (verified
/// this task): an ANSI port declaration (`input logic a`) produces TWO
/// members in `scope.members()` — a `Port`-kind symbol AND its own backing
/// `Net`/`Variable`-kind symbol, immediately adjacent, SHARING THE SAME
/// `SymbolId`** (both have hierarchical path `"m.a"` — `SymbolId` is only
/// the path string, not a true per-AST-node identity). Because `HashMap`'s
/// `FromIterator`/`insert` keeps the LAST value written for a duplicate
/// key, and the backing variable always enumerates immediately AFTER its
/// Port counterpart, this table resolves a colliding `SymbolId` to the
/// BACKING VARIABLE's `SymbolKey` — which is exactly the symbol an ordinary
/// identifier reference inside an expression (`assign y = a & b;`'s `a`/
/// `b`/`y`, or a port-connection's bound actual expression) resolves to via
/// `Expression::referenced_symbol()`/`DfaEvent::symbol` (never the `Port`
/// symbol itself — a module body's expressions never "read" the port
/// declaration node). This table is therefore correct, unmodified, for
/// every caller in this module EXCEPT resolving `PortConnection::port()`
/// itself (a `Port`-kind symbol by definition) back to its own `SymbolKey`
/// — that one case is resolved separately, by name-match against the
/// already-built `Template.ports` (see the port-connection loop below),
/// never through this table.
pub(crate) fn symbol_key_map(
    scope: sv_lang::Symbol<'_>,
) -> HashMap<crate::symid::SymbolId, SymbolKey> {
    crate::netlist::state_items(&scope)
        .into_iter()
        .enumerate()
        .map(|(i, m)| (m.uid(), SymbolKey(i as u32)))
        .collect()
}

/// Task 6 (NL-026): resolves the design's global `sv_lang::TimeScale`.
///
/// **Deviation from the plan's literal spec, documented here (binding for
/// both this function and `tick_ns_per_unit_of` below):** the plan cites
/// `Design::default_time_scale()` as the source. Verified directly against
/// sv-lang's own test suite
/// (`/root/lab/slang/rust/sv-lang/tests/semantic.rs`'s
/// `default_time_scale_is_none_when_not_configured`) and its own doc comment
/// (`ast.rs:1678-1703`) that this getter is *always* `None` at the pinned
/// `sv-lang = "=0.1.9"` version: sv-lang's `Options` builder never sets
/// `CompilationOptions::defaultTimeScale` (no C-API setter exists for it),
/// and — the load-bearing fact — it is documented to NOT be derived from
/// `` `timescale `` source directives at all (a separate, per-scope/
/// per-`Definition` mechanism). Using it would panic (`.unwrap()` on `None`)
/// for every real design and could never differ between two designs whose
/// only difference is their `` `timescale `` directive, which is exactly
/// what NL-025's own acceptance criterion requires. The real per-design
/// "timescale/timeprecision directives" both NL-025 and NL-026 ask for live
/// on `Scope::time_scale()` (`ast.rs:10422-10451`, "the time scale for delay
/// values expressed within this scope... walks the parent-scope chain") —
/// read here off the design's first top instance's own body scope, the same
/// "the design's own scope" this pass already treats as canonical elsewhere
/// (e.g. `resolve_hier_path`'s top-instance walk). Falls back to the LRM
/// §3.14.2.2 default (`1ns` base and precision) only when no `` `timescale ``
/// directive applies anywhere in the design and no compilation default is
/// configured either (`Scope::time_scale()` returns `None`) — never a panic,
/// never a silently-wrong zero value.
fn resolve_time_scale(eval: &sv_lang::EvalSession<'_>) -> sv_lang::TimeScale {
    let from_top_scope = eval
        .design()
        .top_instances()
        .next()
        .and_then(|top| top.instance_body())
        .and_then(|body| body.time_scale());

    from_top_scope.unwrap_or(sv_lang::TimeScale {
        base: sv_lang::TimeScaleValue {
            unit: sv_lang::TimeUnit::Nanoseconds,
            magnitude: 1,
        },
        precision: sv_lang::TimeScaleValue {
            unit: sv_lang::TimeUnit::Nanoseconds,
            magnitude: 1,
        },
    })
}

/// Task 6 (NL-025): derives the tick-per-nanosecond normalization factor
/// (Decision 8) from `time_scale.precision` — the finest unit any `#`/`##`
/// delay in the design can express. One "tick" is one unit of that
/// precision; this returns how many ticks make up one nanosecond (e.g. a
/// `1ps` precision means 1000 ticks per ns, since `1ns == 1000ps`). Both the
/// tick's own length and one nanosecond are first expressed in femtoseconds
/// — finer than every named `TimeUnit` this function handles, so the
/// division below is always between two whole numbers of femtoseconds, never
/// a lossy float — then divided.
///
/// Exhaustive over `TimeUnit` (`#[non_exhaustive]` upstream, so this must
/// end in a wildcard arm to compile at all): a raw unit value sv-lang itself
/// doesn't yet name (`TimeUnit::Other`) is a typed `ElabError::Frontend`,
/// never a silent default/zero (ARCHITECTURE §7 item 9) — likewise a
/// magnitude `0`, or a precision finer than 1ns that does not divide it
/// (a precision coarser than 1ns floors at one tick per nanosecond);
/// returning a silently-truncated/rounded value there would be exactly the
/// kind of "gate that cannot fail" this workspace has already been bitten by
/// once (`gates-that-cannot-fail`).
fn tick_ns_per_unit_of(precision: sv_lang::TimeScaleValue) -> Result<u64, ElabError> {
    use sv_lang::TimeUnit;

    let fs_per_unit: u64 = match precision.unit {
        TimeUnit::Seconds => 1_000_000_000_000_000,
        TimeUnit::Milliseconds => 1_000_000_000_000,
        TimeUnit::Microseconds => 1_000_000_000,
        TimeUnit::Nanoseconds => 1_000_000,
        TimeUnit::Picoseconds => 1_000,
        TimeUnit::Femtoseconds => 1,
        other => {
            return Err(ElabError::Frontend(format!(
                "unrecognized TimeUnit {other:?} in design's time-scale precision"
            )));
        }
    };

    const FS_PER_NS: u64 = 1_000_000;
    let tick_length_fs = fs_per_unit
        .checked_mul(precision.magnitude as u64)
        .ok_or_else(|| {
            ElabError::Frontend(format!(
                "time-scale precision magnitude {} overflows u64 femtoseconds",
                precision.magnitude
            ))
        })?;

    // A precision coarser than 1ns (`1us`, `1ms`) needs no sub-nanosecond tick:
    // the factor floors at one tick per nanosecond (the tick length is the
    // tool's choice, LRM 3.14.3; delays still scale by their own unit).
    if tick_length_fs > FS_PER_NS && tick_length_fs.is_multiple_of(FS_PER_NS) {
        return Ok(1);
    }
    if tick_length_fs == 0 || !FS_PER_NS.is_multiple_of(tick_length_fs) {
        return Err(ElabError::Frontend(format!(
            "design time-scale precision ({:?} x{}) does not evenly divide 1ns into a \
             whole number of ticks",
            precision.unit, precision.magnitude
        )));
    }

    Ok(FS_PER_NS / tick_length_fs)
}

/// Task 2 (NL-004, NL-011, NL-012, NL-013): the hash-then-verify dedup pass.
/// Tags every staged `(instance, body)` pair with its `TemplateKey`
/// (`dedup::template_key_of`), calls `dedup::merge_groups_within_bucket`
/// ONCE over the full tagged list — the exact function
/// `tests/netlist_dedup.rs`'s forced-collision test also calls directly, so
/// there is never a second, independently-reasoned merge implementation —
/// and builds the final, deduped `templates: Vec<Template>`, re-pointing
/// every merged instance's `InstanceNode.template` at the group's shared
/// `TemplateId`. `InstanceTree` itself (`netlist.instances`) is never
/// touched here: dedup collapses `Template`s, never `InstanceNode`s
/// (NL-011/NL-012).
///
/// Task 3 (NL-015, NL-016, NL-018) additions: for every group (including
/// singleton groups — a `Template` used by exactly one instance still goes
/// through this same call, with `others: &[]`), calls
/// `confirm_and_assign_symbol_keys` on the group's bodies — the defensive,
/// fuzz-testable re-check that hard-fails (`ElabError::MergeInvariantViolated`,
/// propagated via `?`, never a panic or silent corruption) if any member
/// disagrees even though `merge_groups_within_bucket`'s own
/// `instance_body_has_same_type` oracle already said the group's bodies
/// match — and records each body's own `Vec<Symbol<'d>>` into
/// `netlist.member_symbols` at that body's own `InstanceId` slot,
/// `SymbolKey`-index-aligned with `confirm_and_assign_symbol_keys`' `shapes`.
///
/// Task 4 (NL-121) addition: a second pass, after every group has assigned
/// its members' final `InstanceNode.template`, builds each `Template`'s own
/// `sub_instances` from its canonical instance's `children`. This MUST be a
/// second pass, not folded into the loop above: `merge_groups_within_bucket`
/// emits groups in first-seen-`TemplateKey` order (== the order of each
/// group's lowest-`InstanceId` member), and a parent instance always has a
/// lower `InstanceId` than any of its children (DFS pre-order numbering), so
/// a parent's `Template` can be — and often is — constructed before its
/// children's own groups have run, leaving their `InstanceNode.template`
/// still unresolved at that point. Every merged instance in one group is
/// already known (by `confirm_and_assign_symbol_keys`'s member-for-member
/// confirmation) to have a structurally identical child-instantiation list,
/// so reading it off the canonical instance alone is sufficient — no other
/// group member needs to be consulted.
fn dedup_templates<'d>(
    netlist: &mut Netlist<'d>,
    staged: &[(sv_lang::Symbol<'d>, sv_lang::Symbol<'d>)],
    eval: &'d sv_lang::EvalSession<'d>,
) -> Result<(), ElabError> {
    let mut defs = DefinitionTable::default();
    let tagged: Vec<(TemplateKey, sv_lang::Symbol<'d>)> = staged
        .iter()
        .map(|(instance, body)| (template_key_of(&mut defs, *instance, *body), *body))
        .collect();

    let groups = merge_groups_within_bucket(&tagged);

    netlist.member_symbols = vec![Vec::new(); staged.len()];
    // Index-aligned with `netlist.templates`: which `InstanceId` was each
    // `Template`'s canonical (group[0]) member, for the `sub_instances`
    // second pass below.
    let mut canonical_instance_of_template: Vec<InstanceId> = Vec::new();

    for group in groups {
        let canonical_index = group[0];
        let (canonical_instance, canonical_body) = staged[canonical_index];
        let key = tagged[canonical_index].0;
        let definition_name = canonical_instance
            .instance_definition()
            .map(|def| def.name().to_string())
            .unwrap_or_default();

        let other_bodies: Vec<sv_lang::Symbol<'d>> = group[1..]
            .iter()
            .map(|&member_index| staged[member_index].1)
            .collect();
        let confirmed = confirm_and_assign_symbol_keys(canonical_body, &other_bodies)?;
        debug_assert_eq!(
            confirmed.per_body_symbols.len(),
            group.len(),
            "one per_body_symbols entry per group member (canonical + every other)"
        );

        // Task 5 (NL-029, NL-030, NL-120, NL-122): `signals`/`memories`/
        // `ports` are built from the SAME canonical body `confirm_and_assign_
        // symbol_keys` just walked above, over the identical flat
        // `members()` enumeration order, so `SymbolKey` assignment is never
        // independently re-derived between the two passes.
        let (signals, memories, ports) = build_members(canonical_body)?;

        // Task 7 (NL-034, NL-037): `processes` (initial/final ->
        // StaticSensitivity::None; always* -> the same placeholder until
        // Task 8) and `continuous_assigns` (this Template's OWN `assign`
        // statements only — synthetic port-connection entries are appended
        // by the second pass below, once every Template in the design has
        // been built). Both walk the SAME canonical body as `build_members`
        // above, over the SAME `SymbolKey` numbering (`scope_keys`).
        let scope_keys = symbol_key_map(canonical_body);
        let processes = build_processes(canonical_body, &scope_keys)?;
        let continuous_assigns = build_own_cont_assigns(eval, canonical_body, &scope_keys)?;

        let template_id = TemplateId(netlist.templates.len() as u32);
        netlist.templates.push(Template {
            key,
            definition_name,
            signals,
            memories,
            processes,
            continuous_assigns,
            // Filled in by the second pass below, once every instance's
            // `InstanceNode.template` in this whole design is resolved.
            sub_instances: Vec::new(),
            ports,
        });
        canonical_instance_of_template.push(InstanceId(canonical_index as u32));

        for (slot, &member_index) in group.iter().enumerate() {
            let instance_id = InstanceId(member_index as u32);
            netlist.instances.node_mut(instance_id).template = template_id;
            netlist.member_symbols[member_index] = confirmed.per_body_symbols[slot].clone();
        }
    }

    for (template_index, &canonical_id) in canonical_instance_of_template.iter().enumerate() {
        let child_ids = netlist.instances.node(canonical_id).children.clone();

        // Task 7 (NL-035, NL-121): the PARENT scope's own SymbolKey table
        // — built the same way `build_own_cont_assigns` built one for this
        // Template's own real `assign` statements above — reused here to
        // resolve every child port connection's driving-expression
        // read-set at THIS (parent) scope.
        let canonical_instance_sym = netlist.instance_symbol(canonical_id);
        let canonical_body = canonical_instance_sym.instance_body().ok_or_else(|| {
            ElabError::Frontend(format!(
                "instance `{}` has no instance_body()",
                canonical_instance_sym.hierarchical_path()
            ))
        })?;
        let parent_scope_keys = symbol_key_map(canonical_body);

        let mut sub_instances = Vec::with_capacity(child_ids.len());
        let mut synthetic_assigns = Vec::new();

        for &child_id in &child_ids {
            let child_template_id = netlist.instances.node(child_id).template;
            let child_instance_sym = netlist.instance_symbol(child_id);

            let mut port_bindings = Vec::new();
            for conn in child_instance_sym.instance_port_connections() {
                let port_symbol = conn.port();
                // An interface port carries no value of its own: the child
                // reaches the connected interface instance's members
                // directly (`b.valid` resolves to `top.bi.valid`).
                if port_symbol.kind() == sv_lang_kinds::SymbolKind::InterfacePort {
                    continue;
                }
                // LRM 23.2.2.1 port expressions: a port that is a part-select,
                // a concatenation (`.p({a, b})`, `{c, d}`) or otherwise not a
                // plain reference to one internal signal is not modelled; the
                // port connection would write the whole internal signal
                // (wrong width) or leave it without a `PortDecl`.
                let plain_internal = port_symbol.kind() == sv_lang_kinds::SymbolKind::Port
                    && port_symbol.port_internal_expr().is_none_or(|e| {
                        matches!(
                            e.kind(),
                            sv_lang_kinds::ExpressionKind::NamedValue
                                | sv_lang_kinds::ExpressionKind::ElementSelect
                                | sv_lang_kinds::ExpressionKind::RangeSelect
                        )
                    });
                if !plain_internal {
                    return Err(ElabError::Frontend(format!(
                        "unsupported: port expression on `{}` (LRM 23.2.2.1: a port that is a \
                         part-select or concatenation of internal signals, or a multi-signal \
                         port, is not supported; declare one port per signal)",
                        port_symbol.hierarchical_path(),
                    )));
                }

                // (Load-bearing finding, TDD'd against real `sv-lang`
                // output: an ANSI port declaration produces TWO members in
                // its owning scope — a `Port`-kind symbol AND its own
                // backing `Net`/`Variable`-kind symbol — and the two SHARE
                // the identical `SymbolId` (hierarchical path), since
                // `SymbolId` is only the path string, not a true per-node
                // identity. `conn.port()` always returns the `Port`-kind
                // symbol (its own doc comment: "a Port, MultiPort, or
                // InterfacePort symbol"), so resolving it through a
                // `SymbolId`-keyed table (`symbol_key_map`, "last write
                // wins") would silently return the BACKING VARIABLE's key
                // instead — wrong for `target`/`port_bindings`, which must
                // be the port's OWN `SymbolKey` (matching `PortDecl.key`,
                // itself `Port`-kind-only). A name-match against the
                // child's own already-built `Template.ports` (unambiguous:
                // port names are unique within one port list) sidesteps
                // the collision entirely and gives `key`+`direction` in one
                // lookup — this is also where `is_output_port_connection`
                // (direction) comes from, per this task's own doc comment
                // on why direction is `PortDecl.direction`-derived, never
                // `Analysis`-dependent. Every child `Template` is already
                // fully materialized by this point (this whole pass runs
                // after the per-group loop above finished building every
                // `Template`, including this child's), so this lookup can
                // never race the child's own construction.
                let child_template = &netlist.templates[child_template_id.0 as usize];
                let (child_port_key, direction) = match child_template
                    .ports
                    .iter()
                    .find(|p| p.name == port_symbol.name())
                {
                    Some(p) => (p.key, p.direction),
                    // LRM 23.2.2.1: a port expression `.a({x, y})` is one
                    // external port; slang splits its connection into one
                    // per internal `Port` (`y <- w[0]`, `x <- w[11:1]`).
                    // Those internal ports are not body members, but each
                    // is backed by the body's own net/variable of the same
                    // name, which the connection drives.
                    None => {
                        let direction = match port_symbol.port_direction() {
                            sv_lang::ArgumentDirection::In => PortDirection::Input,
                            sv_lang::ArgumentDirection::Out => PortDirection::Output,
                            sv_lang::ArgumentDirection::InOut => PortDirection::Inout,
                            sv_lang::ArgumentDirection::Ref => PortDirection::Ref,
                        };
                        let signal = child_template
                            .signals
                            .iter()
                            .find(|sig| sig.name == port_symbol.name())
                            .ok_or_else(|| {
                                ElabError::Frontend(format!(
                                    "port connection `{}` has no matching PortDecl in its \
                                     child Template (a concatenated port expression such as \
                                     `.p({{a, b}})`, LRM 23.2.2.1, is not supported)",
                                    port_symbol.hierarchical_path(),
                                ))
                            })?;
                        (signal.key, direction)
                    }
                };

                // NL-035: the connection's bound expression, resolved at
                // the PARENT scope — `None` (an unconnected port) yields an
                // empty read_set, never an error (a deliberately left-open
                // port is valid SV, not a construct this pass fails on).
                let read_set = match conn.expression() {
                    Some(expr) => expression_read_set(expr, &parent_scope_keys),
                    None => Vec::new(),
                };
                port_bindings.push((child_port_key, read_set.clone()));

                // A `ref` port (LRM 23.3.3.2) is an alias of its actual, not
                // a continuous assignment into it: nothing drives the port.
                if direction == PortDirection::Ref {
                    continue;
                }

                synthetic_assigns.push(ContAssignTemplate {
                    target: child_port_key,
                    read_set,
                    is_output_port_connection: direction == PortDirection::Output,
                });
            }

            sub_instances.push(SubInstanceSlot {
                port_bindings,
                child_template: child_template_id,
            });
        }

        netlist.templates[template_index].sub_instances = sub_instances;
        netlist.templates[template_index]
            .continuous_assigns
            .extend(synthetic_assigns);
    }

    Ok(())
}

/// Task 7 (NL-034): lowers every real `SymbolKind::ContinuousAssign` member
/// of `scope` (the Template's OWN `assign` statements — never a synthetic
/// port-connection entry, `is_output_port_connection` is always `false`
/// here) into a `ContAssignTemplate`. `target` is the assignment's LHS
/// symbol (`continuous_assign_assignment().left().referenced_symbol()`);
/// `read_set` is the RHS read-set via [`cont_assign_read_set`]
/// (`ReadSetLattice`/`run_dataflow`, Decision 1) — `run_dataflow` accepts
/// the `ContinuousAssign` symbol itself as a "procedure" per its own doc
/// comment, so no separate RHS-only symbol needs to be located.
fn build_own_cont_assigns<'d>(
    eval: &sv_lang::EvalSession<'d>,
    scope: sv_lang::Symbol<'d>,
    scope_keys: &HashMap<crate::symid::SymbolId, SymbolKey>,
) -> Result<Vec<ContAssignTemplate>, ElabError> {
    use sv_lang_kinds::SymbolKind;

    let mut out = Vec::new();
    for member in crate::netlist::module_items(&scope) {
        if member.kind() != SymbolKind::ContinuousAssign {
            continue;
        }

        let assignment = member.continuous_assign_assignment().ok_or_else(|| {
            ElabError::Frontend(format!(
                "continuous assign `{}` has no continuous_assign_assignment()",
                member.hierarchical_path()
            ))
        })?;
        let lhs = assignment.left().ok_or_else(|| {
            ElabError::Frontend(format!(
                "continuous assign `{}`'s assignment expression has no left()",
                member.hierarchical_path()
            ))
        })?;
        // A select / concatenation target (`assign w[3:0] = ..`,
        // `assign {hi, lo} = ..`) drives each of its root symbols: one
        // template per root, sharing the assign's read-set.
        let target_symbols = lvalue_roots(lhs);
        if target_symbols.is_empty() {
            return Err(ElabError::Frontend(format!(
                "continuous assign `{}`'s target expression names no variable or net",
                member.hierarchical_path()
            )));
        }
        let rhs = assignment.right().ok_or_else(|| {
            ElabError::Frontend(format!(
                "continuous assign `{}`'s assignment expression has no right()",
                member.hierarchical_path()
            ))
        })?;

        // (Load-bearing finding, TDD'd against real `sv-lang` dataflow
        // output: `run_dataflow` over a `ContinuousAssign` symbol delivers
        // a `DfaEventKind::Read` for the assignment's OWN LHS target
        // BEFORE its `Write` — an artifact of evaluating the target as an
        // lvalue address, not a genuine RHS dependency; e.g. for `assign y
        // = a & b;` the raw event stream is `Read(y), Read(a), Read(b),
        // Write(y)`.) `cont_assign_read_set` excludes exactly that artifact
        // — the `target` read whose event node falls OUTSIDE the RHS's own
        // source span — while keeping any OTHER read of `target` (a genuine
        // RHS self-reference, e.g. `assign y = y | a;`, a legal LRM
        // continuous-assign feedback/latch idiom). A blanket "drop every
        // `target` entry" would silently lose that real dependency, since
        // `ReadSetLattice` used to collapse both to the same
        // `HashSet<SymbolId>` slot.
        for target_symbol in target_symbols {
            // A target in another instance (`assign g.w = ..` through an
            // interface port, or a hierarchical name) is not a driver of
            // this scope; the run pipeline lowers it like any other assign.
            let Some(&target) = scope_keys.get(&target_symbol.uid()) else {
                continue;
            };
            let read_set = if crate::netlist::exprwalk::has_pattern_conditional(rhs) {
                let mut keys = crate::netlist::exprwalk::pattern_rhs_read_set(rhs, scope_keys)
                    .map_err(ElabError::Frontend)?;
                keys.sort();
                keys.dedup();
                keys
            } else {
                cont_assign_read_set(
                    eval,
                    member,
                    target_symbol.uid(),
                    rhs.source_range(),
                    scope_keys,
                )?
            };
            out.push(ContAssignTemplate {
                target,
                read_set,
                is_output_port_connection: false,
            });
        }
    }
    Ok(out)
}

/// The variables/nets an assignment target expression writes: `w` for
/// `w[3:0]` or `s.f`, both `a` and `b` for `{a, b}`.
fn lvalue_roots<'d>(e: sv_lang::Expression<'d>) -> Vec<sv_lang::Symbol<'d>> {
    use sv_lang_kinds::ExpressionKind;
    let kind = e.kind();
    if crate::symid::is_value_name(kind) {
        e.referenced_symbol().into_iter().collect()
    } else if matches!(
        kind,
        ExpressionKind::ElementSelect | ExpressionKind::RangeSelect
    ) {
        e.select_value().map(lvalue_roots).unwrap_or_default()
    } else if kind == ExpressionKind::MemberAccess {
        e.operand().map(lvalue_roots).unwrap_or_default()
    } else if kind == ExpressionKind::Concatenation {
        e.operands().into_iter().flat_map(lvalue_roots).collect()
    } else {
        Vec::new()
    }
}

/// Visits one `Instance` symbol: allocates its (this-task-trivial) 1:1
/// `Template`, records its `InstanceNode` (with a cached `hierarchical_path`
/// — NL-021), then recurses into its body's child instances in declaration
/// order (NL-010) before returning. Every instance — including every one
/// found deep in the hierarchy — is visited exactly once; nothing is
/// flattened into its parent's scope (NL-002).
fn visit_instance<'d>(
    netlist: &mut Netlist<'d>,
    staged: &mut Vec<(sv_lang::Symbol<'d>, sv_lang::Symbol<'d>)>,
    instance: sv_lang::Symbol<'d>,
    parent: Option<InstanceId>,
) -> Result<InstanceId, ElabError> {
    let body = instance.instance_body().ok_or_else(|| {
        ElabError::Frontend(format!(
            "instance `{}` (kind Instance) has no instance_body()",
            instance.hierarchical_path()
        ))
    })?;

    // Cached once, here, per NL-021 — never recomputed by
    // `NetlistView::hierarchical_path`.
    let hier_path = instance.hierarchical_path();
    let name = instance.name().to_string();
    // NL-023: computed once, here, from the real `sv_lang::Symbol` handle —
    // never re-derived from `InstanceNode` data after the fact.
    let array_path = array_path_of(instance)?;

    // Placeholder `TemplateId` (NL-014: every instance is visited and
    // recorded — `staged` below — before Task 2's dedup pass ever runs;
    // `dedup_templates` re-points this at the real, deduped `TemplateId`
    // once the whole tree has been walked). `u32::MAX` rather than `0` so a
    // bug that forgot to re-point a node is loudly wrong (out-of-range),
    // not silently "instance 0's template" by coincidence.
    let placeholder_template = TemplateId(u32::MAX);

    let own_index = netlist.instances.push(InstanceNode {
        id: InstanceId(0), // stamped with the real id immediately below
        template: placeholder_template,
        parent,
        name,
        array_path,
        children: Vec::new(),
    });
    let id = InstanceId(own_index as u32);
    netlist.instances.node_mut(id).id = id;
    netlist.hier_paths.push(hier_path);
    debug_assert_eq!(
        staged.len(),
        own_index,
        "staged must stay index-aligned with InstanceId.0"
    );
    staged.push((instance, body));
    debug_assert_eq!(
        netlist.instance_syms.len(),
        own_index,
        "instance_syms must stay index-aligned with InstanceId.0"
    );
    netlist.instance_syms.push(instance);

    let child_syms = collect_instance_children(body)?;
    let mut child_ids = Vec::with_capacity(child_syms.len());
    for child_sym in child_syms {
        let child_id = visit_instance(netlist, staged, child_sym, Some(id))?;
        child_ids.push(child_id);
    }
    netlist.instances.node_mut(id).children = child_ids;

    Ok(id)
}

/// Walks one instance body's members (transitively through any enclosing
/// `generate for`/instance-array constructs — NL-023) and returns the
/// `Instance`-kind children (in declaration/iteration order — NL-010),
/// rejecting any member kind this v1 elaboration slice cannot account for.
///
/// This match is intentionally *not* a blanket "skip anything that isn't an
/// `Instance`": `SymbolKind` is `#[non_exhaustive]`, so every arm not
/// explicitly listed falls to the wildcard, which returns a typed
/// `ElabError::UnsupportedConstruct` rather than silently treating an
/// unrecognized member as "not an instance, therefore irrelevant" — a
/// construct this pass cannot classify might well be hiding a child
/// instance, and silently skipping it would violate NL-002's "preserve full
/// hierarchy" guarantee. Checkers and primitive instances are still
/// deliberately routed to the error arm (out of this wave's scope); loop-
/// generate blocks and instance arrays (Task 4) are now recognized and
/// recursed into, so the real `Instance` symbols they contain are found and
/// `array_path_of` (called back in `visit_instance`) can derive each one's
/// `array_path` from its true generate-loop/array position.
fn collect_instance_children<'d>(
    body: sv_lang::Symbol<'d>,
) -> Result<Vec<sv_lang::Symbol<'d>>, ElabError> {
    let mut children = Vec::new();
    collect_instance_children_into(body, &mut children)?;
    Ok(children)
}

/// Recursive helper for [`collect_instance_children`]: appends every
/// `Instance`-kind symbol transitively reachable from `scope`'s own
/// `.members()`, descending into `GenerateBlockArray`/`GenerateBlock`/
/// `InstanceArray` members (never flattening them into `Instance` nodes of
/// their own — only real `Instance` symbols become `InstanceNode`s).
fn collect_instance_children_into<'d>(
    scope: sv_lang::Symbol<'d>,
    children: &mut Vec<sv_lang::Symbol<'d>>,
) -> Result<(), ElabError> {
    use sv_lang_kinds::SymbolKind;

    for member in scope.members() {
        match member.kind() {
            SymbolKind::Instance => children.push(member),
            // A true instance-array (`modname name[hi:lo](...)`): descend
            // into its elements, each itself `Instance` (or, for a
            // multi-dimensional array, a nested `InstanceArray`).
            SymbolKind::InstanceArray => collect_instance_array_elements(member, children)?,
            // A `generate for`'s own array-of-blocks symbol: descend into
            // each iteration's `GenerateBlock` entry, which is itself a
            // scope containing that iteration's own declarations.
            SymbolKind::GenerateBlockArray => {
                for entry in member.generate_block_array_entries() {
                    collect_instance_children_into(entry, children)?;
                }
            }
            // A non-loop generate block (`if`/`case`-generate branch) is
            // also a plain scope — descend the same way, UNLESS this is the
            // untaken branch of the `if`/`case` (Task 10, NL-116's own
            // generate-if fixture surfaced this: `Symbol::
            // generate_block_is_uninstantiated()` is `true` for a branch that
            // was never actually elaborated, kept around only so name lookup
            // inside it still type-checks — its own members are
            // `UninstantiatedDef` placeholders, never real `Instance`
            // symbols, verified empirically against pinned `sv-lang =
            // "=0.1.9"` this task: `if (SEL) begin : g_true leaf u(..); end
            // else begin : g_false leaf u(..); end` instantiated with
            // `SEL=1` gives `g_false.generate_block_is_uninstantiated() ==
            // true` and `g_false`'s own `u` member has `kind() ==
            // SymbolKind::UninstantiatedDef`, not `Instance`). Descending
            // into it anyway is wrong twice over: it is not part of the
            // elaborated design at all (NL-002 only promises the REAL
            // elaborated hierarchy), and its placeholder members are not
            // recognized by this match, so descending used to surface as a
            // spurious `ElabError::UnsupportedConstruct` on ordinary,
            // LRM-legal `generate if`/`case` source.
            SymbolKind::GenerateBlock if member.generate_block_is_uninstantiated() => {}
            SymbolKind::GenerateBlock => collect_instance_children_into(member, children)?,
            // Known-benign, non-instantiating members ordinary module
            // bodies contain — explicitly enumerated (never a blanket `_`)
            // so a genuinely unrecognized kind still falls to the error arm
            // below.
            SymbolKind::Port
            | SymbolKind::MultiPort
            | SymbolKind::InterfacePort
            | SymbolKind::Modport
            | SymbolKind::LetDecl
            // An interface `extern task/function` prototype (25.7.1): a
            // declaration, no storage; its exported implementations are
            // resolved at call sites (`crate::netlist::itf`).
            | SymbolKind::MethodPrototype
            | SymbolKind::PrimitiveInstance
            | SymbolKind::NetAlias
            | SymbolKind::ClockingBlock
            | SymbolKind::Net
            | SymbolKind::Variable
            | SymbolKind::ContinuousAssign
            | SymbolKind::Parameter
            | SymbolKind::TypeParameter
            | SymbolKind::Genvar
            | SymbolKind::ProceduralBlock
            | SymbolKind::StatementBlock
            | SymbolKind::Subroutine
            // Named sequence/property declarations (16.8, 16.12) hold no
            // storage and no instances; the assertions that use them are
            // `ProceduralBlock`s (plan T29).
            | SymbolKind::Sequence
            | SymbolKind::Property
            | SymbolKind::ElabSystemTask
            | SymbolKind::TypeAlias
            // A user-defined nettype declaration (LRM 6.6.7): no storage.
            | SymbolKind::NetType
            | SymbolKind::DefParam
            | SymbolKind::Specparam
            // A specify block (`docs/superpowers/plans/2026-10-03-sdf.md`
            // Task 4a) never contains an `Instance` child to collect — its
            // own members (`path_declaration`/`system_timing_check`/
            // `pulsestyle_declaration`/`specparam_declaration`, LRM 30.3's
            // own `specify_item` production) are discovered separately by
            // `crate::netlist::specify::TimingModel::discover`, which
            // itself does not recurse into a NESTED instance (30.3 gives a
            // specify block no such production). Verified live: before
            // this arm existed, ANY design containing `specify ...
            // endspecify` failed HERE, in `run_design`'s very first
            // elaboration pass, before `crate::specify`'s own discovery
            // code ever ran — a real, blocking gap this plan's own "What
            // already exists" survey did not catch.
            | SymbolKind::SpecifyBlock
            // A covergroup declaration holds no instance (its kernels are
            // compiled per instance by `vj-codegen`'s `covergroup`).
            | SymbolKind::CovergroupType
            // A class declared in a module (8.1): its objects live in the
            // object heap, never as instances or module storage.
            | SymbolKind::ClassType
            | SymbolKind::GenericClassDef
            | SymbolKind::ForwardingTypedef
            | SymbolKind::EmptyMember
            | SymbolKind::TransparentMember
            | SymbolKind::DeferredMember
            | SymbolKind::Attribute
            | SymbolKind::WildcardImport
            | SymbolKind::ExplicitImport => {}
            // A checker declaration holds no storage and no instances; its
            // instances are lowered below.
            SymbolKind::Checker => {}
            // A checker instance is a pseudo-instance (17.3): its items
            // run in the enclosing instance (`module_items`).
            // Modules, interfaces and programs cannot be instantiated in a
            // checker (17.2), so only nested checker instances matter.
            SymbolKind::CheckerInstance => collect_instance_children_into(member, children)?,
            // A checker's formal arguments (the inputs are substituted into
            // the body; the outputs are variables of the checker).
            SymbolKind::AssertionPort | SymbolKind::FormalArgument => {}
            other => {
                return Err(ElabError::UnsupportedConstruct {
                    hier_path: member.hierarchical_path(),
                    detail: format!("{other:?}"),
                });
            }
        }
    }
    Ok(())
}

/// Recursive helper for the `InstanceArray` arm above: an array element is
/// either a real `Instance` (the common case) or, for a multi-dimensional
/// array (`modname name[1:0][1:0](...)`), a nested `InstanceArray` — any
/// other element kind is a typed error, never a silent skip.
fn collect_instance_array_elements<'d>(
    array: sv_lang::Symbol<'d>,
    children: &mut Vec<sv_lang::Symbol<'d>>,
) -> Result<(), ElabError> {
    use sv_lang_kinds::SymbolKind;

    for element in array.instance_array_elements() {
        match element.kind() {
            SymbolKind::Instance => children.push(element),
            SymbolKind::InstanceArray => collect_instance_array_elements(element, children)?,
            // A primitive array's elements are primitives, not instances
            // (`vj`'s `primitives` realizes them).
            SymbolKind::PrimitiveInstance => {}
            // An array of checker instances (17.3): each element is a
            // pseudo-instance like a lone one (`module_items`).
            SymbolKind::CheckerInstance => collect_instance_children_into(element, children)?,
            other => {
                return Err(ElabError::UnsupportedConstruct {
                    hier_path: element.hierarchical_path(),
                    detail: format!("instance-array element of kind {other:?}"),
                });
            }
        }
    }
    Ok(())
}

impl<'d> Netlist<'d> {
    /// Task 3 (Deviation #18): the real `sv_lang::Symbol<'d>` handle for one
    /// instance's own copy of a deduped `Template` member, keyed by that
    /// member's `SymbolKey`. `sv_lang::Symbol` is an internal handle type no
    /// external `NetlistView` consumer needs (the trait's public surface
    /// stays exactly `InstanceId`/`TemplateId`/`SymbolKey`-based), so the
    /// plan's own doc comment calls this "crate-private". Deviation from the
    /// plan's literal `pub(crate)`: verified this function must actually be
    /// `pub`, not `pub(crate)` — the plan's own test
    /// `member_symbols_are_distinct_real_symbols_per_instance_not_the_canonical_aliased`
    /// calls `netlist.member_symbol(..)` from `tests/netlist_dedup_invariants.rs`,
    /// an external integration-test crate (`tests/*.rs` files are compiled as
    /// separate crates and only see this crate's `pub` surface — exactly the
    /// reasoning the plan's own `confirm_and_assign_symbol_keys` doc comment
    /// gives for why *that* function is `pub`, immediately above this one).
    /// `pub(crate)` would make that test fail to compile. Still not part of
    /// `NetlistView` and not re-exported from `netlist::mod` beyond
    /// `Netlist` itself, so it remains effectively internal to callers that
    /// hold a concrete `Netlist<'d>` (same crate, or an integration test).
    /// Panics only if called with an `(instance, key)` pair `elaborate()`
    /// itself never produced — every real caller (Task 8, same crate) only
    /// ever passes pairs read off this same `Netlist`'s own
    /// `Template`/`InstanceTree` data, so this can never fire for a correct
    /// caller; a wrong caller is a programmer error this wave chooses to
    /// catch loudly, not a `Result` (this is not an `sv_lang`/`sv_lang_kinds`
    /// kind match, so it is not bound by this plan's "match must fall
    /// through to a typed error" rule — it is an internal indexing
    /// precondition).
    pub fn member_symbol(&self, instance: InstanceId, key: SymbolKey) -> sv_lang::Symbol<'d> {
        self.member_symbols[instance.0 as usize][key.0 as usize]
    }

    /// Task F3 (CG-125/NL-035): the real, undeduped `Instance`-kind
    /// `sv_lang::Symbol<'d>` for `id` — the instantiation site itself (see
    /// `instance_syms`'s own doc comment for why this, not `member_symbol`,
    /// is what the run pipeline needs to reach `instance_body()`/
    /// `instance_port_connections()`). Panics only for an `InstanceId`
    /// `elaborate()` itself never produced, the same indexing-precondition
    /// posture `member_symbol`'s own doc comment documents immediately
    /// above.
    pub fn instance_symbol(&self, id: InstanceId) -> sv_lang::Symbol<'d> {
        self.instance_syms[id.0 as usize]
    }
}

impl<'d> NetlistView for Netlist<'d> {
    fn registration_order(&self) -> Vec<InstanceId> {
        // The `InstanceTree` is populated in exactly this order by
        // `visit_instance`'s DFS above (parent pushed before any of its
        // children) — no separate sort pass exists.
        (0..self.instances.len() as u32).map(InstanceId).collect()
    }

    fn hierarchical_path(&self, id: InstanceId) -> &str {
        &self.hier_paths[id.0 as usize]
    }

    fn resolve_hier_path(&self, path: &str) -> Option<InstantiatedSignalId> {
        // Task 5: a real per-level walk through `InstanceTree`, never a
        // global name->id map (so two same-named children under different
        // parents can never collide — see this task's own
        // `resolve_hier_path_finds_the_correct_signal_on_the_correct_sibling_instance`
        // test). `path` needs at least a top-instance segment and a final
        // signal segment (`"top.sig"`); anything shorter has no signal to
        // resolve to.
        let segments: Vec<&str> = path.split('.').collect();
        let (first, rest) = segments.split_first()?;
        let (last, middle) = rest.split_last()?;

        let mut current_id = *self
            .top
            .iter()
            .find(|&&id| self.instances.node(id).name == *first)?;

        for seg in middle {
            current_id = *self
                .instances
                .node(current_id)
                .children
                .iter()
                .find(|&&child_id| self.instances.node(child_id).name == *seg)?;
        }

        // Ports are intentionally not also searched here: every port already
        // has a same-named internal driving/driven net or variable in this
        // pass's lowering (per NL-034/NL-035's synthetic-ContAssign
        // convention) — searching `signals` alone is sufficient and avoids a
        // double-match against two different `SymbolKey`s for what a caller
        // would call "the same" hierarchical name.
        let template_id = self.instances.node(current_id).template;
        let signal = self.templates[template_id.0 as usize]
            .signals
            .iter()
            .find(|signal| signal.name == *last)?;

        Some(InstantiatedSignalId {
            instance: current_id,
            member: signal.key,
        })
    }

    fn dedup_key(&self, tmpl: TemplateId) -> &TemplateKey {
        &self.templates[tmpl.0 as usize].key
    }

    fn tick_ns_per_unit(&self) -> u64 {
        // Task 6 (NL-025): the real field, computed once in `elaborate()` —
        // see `tick_ns_per_unit_of`'s doc comment.
        self.tick_ns_per_unit
    }

    fn instance(&self, id: InstanceId) -> &InstanceNode {
        self.instances.node(id)
    }

    fn template(&self, id: TemplateId) -> &Template {
        &self.templates[id.0 as usize]
    }
}