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}