Skip to main content

sv_sim_sim/state/
dedup.rs

1//! Task 5 — Template dedup (hash-then-verify) and deterministic traversal
2//! order (ledger ST-059, ST-064, ST-065, ST-066).
3//!
4//! `TemplateTable::build` walks every concrete instance reachable from
5//! `design.top_instances()` (via `Symbol::instance_body()`/`members()`'s
6//! `Instance`/`InstanceArray`-kind entries, expanding
7//! `instance_array_elements()` for any array-of-instances declaration along
8//! the way -- recursing through nested `InstanceArray` elements for a
9//! multi-dimensional array -- and recursing into `GenerateBlock`/
10//! `GenerateBlockArray`-kind members for `if`/`case`/`for`-generate content,
11//! which is the actual `SymbolKind` a `generate for` loop of instances
12//! elaborates to, skipping any generate branch
13//! `Symbol::generate_block_is_uninstantiated` reports as not taken),
14//! building a [`super::StateLayout`] for each one (Task 4/6's
15//! `build_template_layout`, called once per concrete instance — ST-065's own
16//! text: `Compilation::compile()` forces `DisableInstanceCaching`, so slang
17//! never pointer-shares two structurally-identical bodies and every concrete
18//! instance really must be individually walked before dedup can collapse
19//! them) and bucketing instances into shared templates by a `(Definition
20//! identity, structural-shape hash)` key, confirmed pairwise via
21//! `Symbol::instance_body_has_same_type` before a hash-bucket hit is ever
22//! accepted as a true dedup — hash-then-verify, never pointer identity
23//! (ST-065).
24//!
25//! **Review correction (r2):** an earlier revision treated
26//! `design.top_instances()`'s own elements as traversal *roots only* —
27//! registering every descendant instance's body as a `TemplateTable` entry
28//! but never a root's own body. That directly contradicted
29//! `docs/design/state.md` §2's stated walk ("Template tables ... Built by
30//! walking `design.top_instances()` → `Symbol::instance_body()` →
31//! `Symbol::members()` recursively into child instances ..., collecting
32//! every member with `sym.kind()` in `{Variable, Net, Port}`" — the very
33//! first `members()` call in that description *is* on a top instance's own
34//! body): any `Variable`/`Net`/`Port` declared directly inside a top module
35//! (e.g. `module top; logic clk; leaf a(clk); endmodule`) was silently and
36//! permanently dropped from the entire state subsystem — never captured in
37//! any `StateLayout`, never reachable via `layout_of`/`slot_of`, with no
38//! error and (before this revision) no test catching it. `Symbol::top_instances()` returns ordinary `Instance`-kind
39//! `Symbol`s (see `sv_lang::Design::top_instances()`'s own doc example, which
40//! calls `.instance_body()` on the returned item exactly like any other
41//! `Instance` symbol reached via `members()`), so `build` below now feeds
42//! each one through [`register_instance`](TemplateTable::register_instance)
43//! directly — the same hash-then-verify registration, definition-index
44//! bookkeeping, and `instance_order` bookkeeping as any other concrete
45//! instance — rather than hand-unwrapping its body and skipping straight to
46//! [`walk_children`](TemplateTable::walk_children). A top module with no
47//! local `Variable`/`Net`/`Port` members of its own (the common case) still
48//! gets a real, if zero-slot, template entry: this is not a special case,
49//! it is exactly how an intermediate (non-top, non-leaf) module with no
50//! local members already behaved before this fix, so top-level bodies are
51//! now treated uniformly with every other instance body instead of being
52//! the one silent exception. This does change `templates.len()`/
53//! `instance_order` literal counts for a design with N top modules (each
54//! top now contributes its own template-table entry and one `instance_order`
55//! entry) — every test below that asserted a literal count has been updated
56//! to account for the (now-registered) top-level template(s), and a new
57//! test (`top_level_modules_own_members_get_a_state_layout`) proves a
58//! top-level `Variable` is actually captured, not just that the counts
59//! changed.
60
61use super::layout::{LayoutError, build_template_layout};
62use super::{SlotClass, StateLayout};
63use crate::netlist::TemplateKey;
64use std::collections::HashMap;
65use std::collections::hash_map::DefaultHasher;
66use std::hash::{Hash, Hasher};
67use sv_lang::kinds::SymbolKind;
68use sv_lang::{Design, Symbol};
69
70/// One entry per unique `(module,params)` template; `templates`' insertion
71/// order is first-seen order during the deterministic DFS walk below —
72/// ST-064's "identical offsets across runs" precondition falls out of that
73/// determinism, not a separate sort.
74pub struct TemplateTable<'d> {
75    templates: Vec<(TemplateKey, StateLayout)>,
76    /// The representative `InstanceBody` `Symbol` first registered under
77    /// each `templates` entry's key, kept parallel to `templates` by index —
78    /// the ground truth `instance_body_has_same_type` verifies every later
79    /// hash-bucket hit against (ST-065: hash-then-verify, never pointer
80    /// identity).
81    representatives: Vec<Symbol<'d>>,
82    /// One `TemplateKey` per concrete instance discovered, in the exact DFS
83    /// order this walk produced them (ST-064's "walk order" query; consumed
84    /// later by Task 7's per-instance base-assignment driver, which needs one
85    /// entry per concrete instance, not one per unique template).
86    instance_order: Vec<TemplateKey>,
87}
88
89impl<'d> TemplateTable<'d> {
90    /// Walks every concrete instance reachable from `design.top_instances()`
91    /// (ST-059), assigning each a [`TemplateKey`] via hash-then-verify dedup
92    /// (ST-065) and recording the DFS visitation order (ST-064).
93    pub fn build(design: &'d Design) -> Result<TemplateTable<'d>, LayoutError> {
94        let mut table = TemplateTable {
95            templates: Vec::new(),
96            representatives: Vec::new(),
97            instance_order: Vec::new(),
98        };
99        let mut definitions: Vec<Symbol<'d>> = Vec::new();
100        let mut buckets: HashMap<TemplateKey, usize> = HashMap::new();
101
102        for top in design.top_instances() {
103            // Review correction (r2): `top` is itself an `Instance`-kind
104            // `Symbol` (see `sv_lang::Design::top_instances()`'s own doc
105            // example), so it is registered exactly like any other concrete
106            // instance discovered via `walk_children` below — not merely
107            // unwrapped for its body and skipped straight to walking
108            // children. This is what makes a top module's own
109            // `Variable`/`Net`/`Port` members (e.g. `module top; logic clk;
110            // ...; endmodule`) get a `StateLayout`/`TemplateKey` per
111            // `docs/design/state.md` §2's walk, instead of being silently
112            // dropped. `register_instance` itself calls `walk_children` on
113            // `top`'s body as its last step, so descendants are still
114            // walked exactly as before.
115            table.register_instance(top, &mut definitions, &mut buckets)?;
116        }
117
118        Ok(table)
119    }
120
121    /// Visits `body`'s `Instance`/`InstanceArray`/`GenerateBlock`/
122    /// `GenerateBlockArray`-kind members (every other kind — `Variable`,
123    /// `Net`, `Port`, `Genvar`, ... — is this walk's own business only
124    /// through [`build_template_layout`], never re-walked here) and
125    /// registers each concrete instance found, recursing through generate
126    /// constructs since slang elaborates `if`/`case`/`for`-generate content
127    /// as `GenerateBlock`/`GenerateBlockArray` symbols, not as
128    /// `Instance`/`InstanceArray` ones.
129    fn walk_children(
130        &mut self,
131        body: Symbol<'d>,
132        definitions: &mut Vec<Symbol<'d>>,
133        buckets: &mut HashMap<TemplateKey, usize>,
134    ) -> Result<(), LayoutError> {
135        for member in body.members() {
136            match member.kind() {
137                SymbolKind::Instance => self.register_instance(member, definitions, buckets)?,
138                SymbolKind::InstanceArray => {
139                    // ST-059: an array-of-instances element is a distinct
140                    // concrete instance that can legitimately dedup
141                    // against, or differ from, its array siblings — each
142                    // one is registered individually, never as one shared
143                    // entry for the whole array. A multi-dimensional array's
144                    // elements can themselves be nested `InstanceArray`
145                    // symbols (one level per extra dimension), so dispatch
146                    // through `register_array_element` rather than assuming
147                    // every element is a concrete `Instance`.
148                    for elem in member.instance_array_elements() {
149                        self.register_array_element(elem, definitions, buckets)?;
150                    }
151                }
152                SymbolKind::GenerateBlock => {
153                    // ST-059: the ordinary way an `if`/`case`-generate
154                    // construct elaborates. `generate_block_is_uninstantiated`
155                    // reports true for a branch that was NOT selected during
156                    // elaboration (e.g. the `else` side of a taken `if`) —
157                    // skip it so an untaken branch contributes nothing,
158                    // rather than walking its (possibly incomplete) member
159                    // list.
160                    if !member.generate_block_is_uninstantiated() {
161                        self.walk_children(member, definitions, buckets)?;
162                    }
163                }
164                SymbolKind::GenerateBlockArray => {
165                    // ST-059: what an ordinary (labeled or unlabeled)
166                    // `generate for` loop of instances actually elaborates
167                    // to — one `GenerateBlock` per loop iteration, each
168                    // walked exactly like any other generate block.
169                    for entry in member.generate_block_array_entries() {
170                        if !entry.generate_block_is_uninstantiated() {
171                            self.walk_children(entry, definitions, buckets)?;
172                        }
173                    }
174                }
175                _ => {}
176            }
177        }
178        Ok(())
179    }
180
181    /// Dispatches one element of an `InstanceArray`'s `elements()` list: a
182    /// concrete `Instance` is registered directly, and a nested
183    /// `InstanceArray` (one extra dimension of a multi-dimensional array,
184    /// e.g. `leaf arr[1:0][1:0]();`) is expanded recursively. A
185    /// `PrimitiveInstance`/`CheckerInstance` element (slang's other two
186    /// `InstanceArray`-element kinds) carries no module-shaped
187    /// `InstanceBody` the way an `Instance` does, so — like
188    /// [`build_template_layout`]'s own `Variable`/`Net`/`Port`-only member
189    /// filter — it is intentionally out of this task's scope rather than an
190    /// error.
191    fn register_array_element(
192        &mut self,
193        elem: Symbol<'d>,
194        definitions: &mut Vec<Symbol<'d>>,
195        buckets: &mut HashMap<TemplateKey, usize>,
196    ) -> Result<(), LayoutError> {
197        match elem.kind() {
198            SymbolKind::Instance => self.register_instance(elem, definitions, buckets)?,
199            SymbolKind::InstanceArray => {
200                for nested in elem.instance_array_elements() {
201                    self.register_array_element(nested, definitions, buckets)?;
202                }
203            }
204            _ => {}
205        }
206        Ok(())
207    }
208
209    /// Registers one concrete `Instance` symbol: builds its `StateLayout`,
210    /// buckets it into a template via hash-then-verify dedup, records its
211    /// `TemplateKey` in `instance_order`, and recurses into its children.
212    /// Called both from [`walk_children`](Self::walk_children)'s
213    /// `Instance`-kind dispatch for an ordinary descendant instance, and
214    /// directly from [`build`](Self::build) for each of
215    /// `design.top_instances()`'s own elements (review r2 fix) — a top
216    /// instance is registered identically to any other, so its own
217    /// `Variable`/`Net`/`Port` members are captured too, not just those of
218    /// its descendants.
219    fn register_instance(
220        &mut self,
221        inst: Symbol<'d>,
222        definitions: &mut Vec<Symbol<'d>>,
223        buckets: &mut HashMap<TemplateKey, usize>,
224    ) -> Result<(), LayoutError> {
225        let body = inst
226            .instance_body()
227            .expect("an Instance symbol always has a body");
228        // ST-065: every concrete instance is individually walked/hashed —
229        // `Compilation::compile()` forces `DisableInstanceCaching`, so slang
230        // itself never pointer-shares two structurally-identical bodies;
231        // this build (Task 4/6's own entry point) is the thing that makes a
232        // hash bucketing possible at all.
233        let layout = build_template_layout(&body)?;
234        let definition = body
235            .instance_body_definition()
236            .expect("an InstanceBody symbol always has a Definition");
237        let definition_index = match definitions.iter().position(|d| *d == definition) {
238            Some(i) => i as u32,
239            None => {
240                definitions.push(definition);
241                (definitions.len() - 1) as u32
242            }
243        };
244        let mut key = TemplateKey {
245            definition: definition_index,
246            params_hash: structural_shape_hash(&layout),
247        };
248
249        // Hash-then-verify (ST-065): bucket by `key` first — a plain
250        // HashMap lookup, never a call into slang — and only when that
251        // lookup hits does this function call `instance_body_has_same_type`
252        // to confirm the hit is a true structural match, not a hash
253        // collision. A verify miss perturbs the hash and retries rather than
254        // silently merging two different templates.
255        let resolved_index = loop {
256            match buckets.get(&key) {
257                Some(&idx) if body.instance_body_has_same_type(&self.representatives[idx]) => {
258                    break idx;
259                }
260                Some(_) => {
261                    key.params_hash = key.params_hash.wrapping_add(0x9E37_79B9_7F4A_7C15);
262                }
263                None => {
264                    let idx = self.templates.len();
265                    self.templates.push((key, layout));
266                    self.representatives.push(body);
267                    buckets.insert(key, idx);
268                    break idx;
269                }
270            }
271        };
272
273        let resolved_key = self.templates[resolved_index].0;
274        self.instance_order.push(resolved_key);
275
276        self.walk_children(body, definitions, buckets)
277    }
278
279    /// Template-relative layout lookup — never a per-instance storage offset
280    /// (ST-066: sharing a template means sharing layout, never storage;
281    /// per-instance base assignment is Task 7's job, not this module's).
282    pub fn layout_of(&self, key: &TemplateKey) -> Option<&StateLayout> {
283        self.templates
284            .iter()
285            .find(|(k, _)| k == key)
286            .map(|(_, layout)| layout)
287    }
288
289    /// The exact DFS order concrete instances were discovered in (ST-064) —
290    /// one entry per concrete instance, not one per unique template.
291    pub fn walk_order(&self) -> impl Iterator<Item = &TemplateKey> {
292        self.instance_order.iter()
293    }
294}
295
296fn slot_class_discriminant(class: SlotClass) -> u8 {
297    match class {
298        SlotClass::Inline => 0,
299        SlotClass::Wide => 1,
300        SlotClass::Memory => 2,
301        // Review fix (Task 14 r2, blocking finding 2): `force_driver`'s
302        // synthetic contribution-cell class. The layout builder this
303        // function's own callers key off of never produces it (see
304        // `SlotClass::ForceWide`'s own doc comment), but the discriminant
305        // must still be total over the enum.
306        SlotClass::ForceWide => 3,
307    }
308}
309
310/// A deterministic, order-sensitive stand-in for "resolved parameter values"
311/// (ST-065's own text: the real resolved-parameter hash is the `netlist`
312/// track's job once it exists; this hash only needs to be a deterministic
313/// function of what this crate can already observe — the concrete,
314/// parameter-substituted [`StateLayout`] this task builds anyway, which
315/// already reflects a `logic [W-1:0]`-style width difference between two
316/// differently-parameterized instances of the same module).
317fn structural_shape_hash(layout: &StateLayout) -> u64 {
318    let mut hasher = DefaultHasher::new();
319    layout.slots.len().hash(&mut hasher);
320    for slot in &layout.slots {
321        slot_class_discriminant(slot.offset.class).hash(&mut hasher);
322        slot.offset.bit_width.hash(&mut hasher);
323        slot.offset.elem_word_count.hash(&mut hasher);
324        slot.offset.row_stride_words.hash(&mut hasher);
325        slot.offset.num_rows.hash(&mut hasher);
326    }
327    layout.inline_words.hash(&mut hasher);
328    layout.wide_words.hash(&mut hasher);
329    hasher.finish()
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use crate::state::value::compile_source;
336
337    /// ST-059/ST-065: two textually-identical instances of the same module
338    /// must share exactly one template. `top` itself is now also registered
339    /// as its own (zero-slot) template (review r2 fix), so the table holds 2
340    /// entries total — `top`'s own and `leaf`'s shared one — but `a` and `b`
341    /// must still resolve to the exact same `TemplateKey`. Fails if dedup
342    /// incorrectly walks `a`/`b` as separate templates (3 total instead of
343    /// 2), or if `top`'s own template collapses into `leaf`'s (2 keys equal
344    /// when they must differ).
345    #[test]
346    fn two_textually_identical_instances_share_one_template() {
347        let design = compile_source(
348            "module leaf; logic x; endmodule\n module top; leaf a(); leaf b(); endmodule\n",
349        )
350        .expect("valid SV source compiles");
351
352        let table = TemplateTable::build(&design).expect("dedup walk builds");
353        assert_eq!(
354            table.templates.len(),
355            2,
356            "top's own (zero-slot) template plus leaf's one shared template"
357        );
358
359        let keys: Vec<TemplateKey> = table.walk_order().copied().collect();
360        assert_eq!(
361            keys.len(),
362            3,
363            "one TemplateKey per concrete instance: top itself, a, b"
364        );
365        assert_eq!(
366            keys[1], keys[2],
367            "leaf a() and leaf b() must dedup into the same template"
368        );
369        assert_ne!(
370            keys[0], keys[1],
371            "top's own template must be distinct from leaf's"
372        );
373    }
374
375    /// ST-065: two instances of the same module with different resolved
376    /// parameters must NOT collapse into one template, even though the
377    /// hash-then-verify path is the same code as the identical-instance
378    /// case above. Fails if the dedup step (or a naive pointer-identity
379    /// dedup) collapses these into one template despite
380    /// `instance_body_has_same_type` correctly reporting them as different.
381    #[test]
382    fn differently_parameterized_instances_get_distinct_templates() {
383        let design = compile_source(
384            "module leaf #(parameter int W=1); logic [W-1:0] x; endmodule\n\
385             module top; leaf #(.W(4)) a(); leaf #(.W(8)) b(); endmodule\n",
386        )
387        .expect("valid SV source compiles");
388
389        let table = TemplateTable::build(&design).expect("dedup walk builds");
390        assert_eq!(
391            table.templates.len(),
392            3,
393            "top's own zero-slot template plus two differently-parameterized leaf templates"
394        );
395
396        // `top`'s own template has zero slots (no local Variable/Net/Port),
397        // so it is filtered out here rather than indexed into directly —
398        // this is exactly what distinguishes it from the two leaf templates
399        // being compared.
400        let mut widths: Vec<u32> = table
401            .templates
402            .iter()
403            .filter_map(|(_, layout)| layout.slots.first().map(|s| s.offset.bit_width))
404            .collect();
405        widths.sort_unstable();
406        assert_eq!(
407            widths,
408            vec![4, 8],
409            "the two templates' single slot must reflect W=4 and W=8 respectively"
410        );
411    }
412
413    /// ST-064: building `TemplateTable` twice from the *same* `Design` must
414    /// assign byte-identical `(TemplateKey, offset)` pairs both times — the
415    /// literal bit-identical-determinism-across-runs precondition. Fails if
416    /// traversal order (and therefore definition-index/hash assignment)
417    /// ever depends on something not guaranteed stable, such as a
418    /// `HashMap`'s iteration order, instead of `members()`'s
419    /// guaranteed-stable declaration order.
420    #[test]
421    fn building_twice_from_the_same_design_assigns_identical_offsets() {
422        let design = compile_source(
423            "module leaf #(parameter int W=1); logic [W-1:0] x; endmodule\n\
424             module top; leaf #(.W(4)) a(); leaf #(.W(8)) b(); leaf #(.W(4)) c(); endmodule\n",
425        )
426        .expect("valid SV source compiles");
427
428        let first = TemplateTable::build(&design).expect("first dedup walk builds");
429        let second = TemplateTable::build(&design).expect("second dedup walk builds");
430
431        assert_eq!(first.templates.len(), second.templates.len());
432        assert_eq!(first.instance_order, second.instance_order);
433
434        for ((k1, l1), (k2, l2)) in first.templates.iter().zip(second.templates.iter()) {
435            assert_eq!(k1, k2);
436            assert_eq!(l1.slots.len(), l2.slots.len());
437            assert_eq!(l1.inline_words, l2.inline_words);
438            assert_eq!(l1.wide_words, l2.wide_words);
439            for (s1, s2) in l1.slots.iter().zip(l2.slots.iter()) {
440                assert_eq!(s1.offset.class, s2.offset.class);
441                assert_eq!(s1.offset.word_offset, s2.offset.word_offset);
442                assert_eq!(s1.offset.bit_width, s2.offset.bit_width);
443            }
444        }
445    }
446
447    /// ST-066: sharing a template means sharing layout, never storage.
448    /// `leaf a(); leaf b();` share one `TemplateKey`/`StateLayout` (already
449    /// proven above); this test additionally confirms `layout_of` hands back
450    /// the exact same `&StateLayout` reference from both `a`'s and `b`'s
451    /// perspective (`std::ptr::eq`), never two independently re-derived
452    /// copies — proving by construction that per-instance base assignment
453    /// (which is what would make storage distinct) is not this module's job
454    /// at all (`layout_of`'s own signature returns only a template-relative
455    /// `&StateLayout`, never a per-instance offset).
456    #[test]
457    fn sharing_a_template_means_sharing_layout_never_storage() {
458        let design = compile_source(
459            "module leaf; logic x; endmodule\n module top; leaf a(); leaf b(); endmodule\n",
460        )
461        .expect("valid SV source compiles");
462
463        let table = TemplateTable::build(&design).expect("dedup walk builds");
464        assert_eq!(
465            table.templates.len(),
466            2,
467            "top's own (zero-slot) template plus leaf's one shared template"
468        );
469
470        let keys: Vec<TemplateKey> = table.walk_order().copied().collect();
471        assert_eq!(
472            keys.len(),
473            3,
474            "one TemplateKey per concrete instance (top itself, a, b)"
475        );
476        assert_eq!(
477            keys[1], keys[2],
478            "a and b must resolve to the same TemplateKey"
479        );
480
481        let from_a = table
482            .layout_of(&keys[1])
483            .expect("a's template has a layout");
484        let from_b = table
485            .layout_of(&keys[2])
486            .expect("b's template has a layout");
487        assert!(
488            std::ptr::eq(from_a, from_b),
489            "layout_of must hand back the exact same StateLayout, never a re-derived copy"
490        );
491    }
492
493    /// ST-059: a labeled `generate for` loop of instances elaborates to
494    /// `GenerateBlockArray`/`GenerateBlock` symbols, not `InstanceArray`.
495    /// Fails (pre-fix) with `templates.len() == 0` and
496    /// `instance_order.len() == 0` — the whole loop body silently vanishes.
497    #[test]
498    fn labeled_generate_for_loop_instances_are_discovered_and_deduped() {
499        let design = compile_source(
500            "module leaf; logic x; endmodule\n\
501             module top;\n\
502               genvar i;\n\
503               generate\n\
504                 for (i = 0; i < 3; i = i + 1) begin : gf\n\
505                   leaf u();\n\
506                 end\n\
507               endgenerate\n\
508             endmodule\n",
509        )
510        .expect("valid SV source compiles");
511
512        let table = TemplateTable::build(&design).expect("dedup walk builds");
513        assert_eq!(
514            table.templates.len(),
515            2,
516            "top's own zero-slot template plus the one leaf template all 3 generate-for instances share"
517        );
518        assert_eq!(
519            table.instance_order.len(),
520            4,
521            "one TemplateKey per concrete instance: top itself plus 3 generate-for instances"
522        );
523    }
524
525    /// ST-059: the unlabeled `generate for` idiom (`for (...) begin ... end`
526    /// with no `: label`) is the single most common instance-array idiom in
527    /// real RTL; it elaborates to an anonymous `GenerateBlockArray`
528    /// (`genblk1`) exactly like the labeled form. Fails (pre-fix) with 0
529    /// instances discovered.
530    #[test]
531    fn unlabeled_generate_for_loop_instances_are_discovered() {
532        let design = compile_source(
533            "module leaf; logic x; endmodule\n\
534             module top;\n\
535               genvar i;\n\
536               generate\n\
537                 for (i = 0; i < 3; i = i + 1) begin\n\
538                   leaf u();\n\
539                 end\n\
540               endgenerate\n\
541             endmodule\n",
542        )
543        .expect("valid SV source compiles");
544
545        let table = TemplateTable::build(&design).expect("dedup walk builds");
546        assert_eq!(
547            table.templates.len(),
548            2,
549            "top's own zero-slot template plus the one leaf template all 3 generate-for instances share"
550        );
551        assert_eq!(
552            table.instance_order.len(),
553            4,
554            "one TemplateKey per concrete instance: top itself plus 3 generate-for instances"
555        );
556    }
557
558    /// ST-059: an `if`-generate conditional instance must be discovered when
559    /// its branch is taken, and the untaken branch (reported by
560    /// `Symbol::generate_block_is_uninstantiated`) must contribute nothing —
561    /// not a phantom instance, not an error. Fails (pre-fix) with 0
562    /// instances discovered for the taken branch (the bug), or would fail if
563    /// the untaken branch were walked anyway (walking an uninstantiated
564    /// generate block is not guaranteed safe/empty).
565    #[test]
566    fn if_generate_conditional_instance_is_discovered_and_untaken_branch_contributes_nothing() {
567        let design = compile_source(
568            "module leaf; logic x; endmodule\n\
569             module top;\n\
570               generate\n\
571                 if (1) begin : g_taken\n\
572                   leaf a();\n\
573                 end else begin : g_untaken\n\
574                   leaf b();\n\
575                 end\n\
576               endgenerate\n\
577             endmodule\n",
578        )
579        .expect("valid SV source compiles");
580
581        let table = TemplateTable::build(&design).expect("dedup walk builds");
582        assert_eq!(
583            table.templates.len(),
584            2,
585            "top's own zero-slot template plus the taken if-generate branch's leaf template"
586        );
587        assert_eq!(
588            table.instance_order.len(),
589            2,
590            "top itself plus the one taken-branch instance; the untaken else-generate branch must contribute zero"
591        );
592    }
593
594    /// A multi-dimensional `InstanceArray`'s elements are themselves nested
595    /// `InstanceArray` symbols for every dimension but the innermost — only
596    /// the final level is actual `Instance` symbols. Fails (pre-fix) with a
597    /// panic ("an Instance symbol always has a body") because
598    /// `register_instance` was called directly on an outer-dimension
599    /// `InstanceArray` element instead of recursing into it.
600    #[test]
601    fn multi_dimensional_instance_array_elements_are_discovered_without_panicking() {
602        let design = compile_source(
603            "module leaf; logic x; endmodule\n\
604             module top;\n\
605               leaf arr[1:0][1:0]();\n\
606             endmodule\n",
607        )
608        .expect("valid SV source compiles");
609
610        let table = TemplateTable::build(&design).expect("dedup walk builds");
611        assert_eq!(
612            table.templates.len(),
613            2,
614            "top's own zero-slot template plus the one template all 4 array elements dedup into"
615        );
616        assert_eq!(
617            table.instance_order.len(),
618            5,
619            "top itself plus one TemplateKey per concrete array element (2x2 = 4)"
620        );
621    }
622
623    /// Review r2 fix: a top-level module's own `Variable`/`Net`/`Port`
624    /// members must be captured in a `StateLayout`, exactly like any other
625    /// instance body's members — `docs/design/state.md` §2's walk explicitly
626    /// starts with `design.top_instances()` → `instance_body()` →
627    /// `members()`, and that first `members()` call is on `top`'s own body.
628    /// Before the fix, `top` was only ever a traversal root (never itself
629    /// registered via `register_instance`/`build_template_layout`), so
630    /// `clk` here would be silently and permanently dropped: not present in
631    /// any `StateLayout`, not reachable via `layout_of`, and
632    /// `table.walk_order()` would report only 1 concrete instance (`a`)
633    /// instead of 2 (`top`, `a`). Fails (pre-fix) on the `keys.len() == 2`
634    /// assertion alone.
635    #[test]
636    fn top_level_modules_own_members_get_a_state_layout() {
637        let design = compile_source(
638            "module leaf; logic y; endmodule\n\
639             module top; logic clk; leaf a(); endmodule\n",
640        )
641        .expect("valid SV source compiles");
642
643        let table = TemplateTable::build(&design).expect("dedup walk builds");
644
645        let keys: Vec<TemplateKey> = table.walk_order().copied().collect();
646        assert_eq!(
647            keys.len(),
648            2,
649            "one TemplateKey per concrete instance: top itself, a"
650        );
651        assert_ne!(
652            keys[0], keys[1],
653            "top's own template (with `clk`) must differ from leaf's (with `y`)"
654        );
655
656        let top_layout = table
657            .layout_of(&keys[0])
658            .expect("top's own body must have a registered StateLayout");
659        assert_eq!(
660            top_layout.slots.len(),
661            1,
662            "top's own `clk` variable must be captured as a slot, not silently dropped"
663        );
664
665        let leaf_layout = table
666            .layout_of(&keys[1])
667            .expect("leaf's body must have a registered StateLayout");
668        assert_eq!(
669            leaf_layout.slots.len(),
670            1,
671            "leaf's own `y` variable must still be captured as a slot"
672        );
673    }
674}