sv_sim_sim/netlist/dedup.rs
1//! Task 2 (NL-004, NL-011, NL-012, NL-013): `TemplateKey` hash-then-verify
2//! `Template` dedup (Decision 2). `Design::compile()` forces
3//! `DisableInstanceCaching` (ARCHITECTURE §7 item 7): `sv-lang` itself never
4//! pointer-shares two instance bodies, even textually identical ones, so
5//! this module reconstructs that sharing at the netlist layer instead.
6//! `TemplateKey` (built by [`template_key_of`]) is a candidate-bucketing
7//! filter ONLY — [`merge_groups_within_bucket`] never merges two bodies on
8//! `TemplateKey` equality alone; every same-key pair is confirmed pairwise
9//! via `Symbol::instance_body_has_same_type` (the ground truth) before
10//! merging.
11
12use crate::netlist::{ElabError, SymbolKey, TemplateKey};
13use crate::symid::SymbolUid;
14use std::collections::HashMap;
15use std::hash::{Hash, Hasher};
16
17/// Interns a definition `Symbol` (`instance_body_definition()`) to a stable
18/// dense id, keyed by `SymbolUid::uid() -> crate::symid::SymbolId` — the
19/// hierarchical-path-keyed, `Hash + Eq + Clone` stable key `sv-lang` itself
20/// publishes for exactly this purpose (verified at pinned `sv-lang = "=0.1.9"`,
21/// `ast.rs:5205-5226`).
22#[derive(Default)]
23pub(crate) struct DefinitionTable {
24 by_id: HashMap<crate::symid::SymbolId, u32>,
25}
26
27impl DefinitionTable {
28 pub(crate) fn intern(&mut self, def: sv_lang::Symbol<'_>) -> u32 {
29 let next_id = self.by_id.len() as u32;
30 *self.by_id.entry(def.uid()).or_insert(next_id)
31 }
32}
33
34/// Builds a `TemplateKey` (Decision 2: hash-then-verify, not pointer
35/// identity) from an instantiated body's `instance_body_definition()` (the
36/// `Definition`-identity half) and the owning instance's elaborated
37/// `parameters()` (the resolved-parameter half). `params_hash` is a
38/// candidate-bucketing filter only — `elaborate()`'s dedup pass NEVER merges
39/// two bodies on `TemplateKey` equality alone; every same-key pair is
40/// confirmed pairwise via `Symbol::instance_body_has_same_type` before
41/// merging (ground truth per Decision 2 — hash quality only affects
42/// performance).
43///
44/// Deviation from the plan's illustrative signature
45/// (`fn template_key_of(defs: &mut DefinitionTable, body: Symbol<'_>) ->
46/// TemplateKey`, taking only the instance `body`): verified against pinned
47/// `sv-lang = "=0.1.9"` that `Symbol::parameters()`'s own doc/SAFETY comment
48/// ("count is 0 for non-instances") means it is populated only when called
49/// on an `Instance`-kind symbol, never on that instance's `InstanceBody`
50/// (`body`, `SymbolKind::InstanceBody`) — there is no way to read the
51/// resolved parameter list starting from `body` alone. This function
52/// therefore additionally takes the owning `instance` (`Instance`-kind)
53/// symbol so it can call `instance.parameters()` directly; `body`
54/// (`instance.instance_body()`) still supplies the `Definition` identity
55/// half via `instance_body_definition()`, exactly as planned.
56pub(crate) fn template_key_of(
57 defs: &mut DefinitionTable,
58 instance: sv_lang::Symbol<'_>,
59 body: sv_lang::Symbol<'_>,
60) -> TemplateKey {
61 let definition = body
62 .instance_body_definition()
63 .map(|def| defs.intern(def))
64 .unwrap_or(u32::MAX);
65
66 // Deterministic (fixed-key) hasher: `DefaultHasher::new()` is NOT the
67 // per-process-randomized `RandomState` `HashMap` uses — it hashes the
68 // same input to the same output across runs within one Rust std build,
69 // which is what NL-004's "stable across elaborate() runs" acceptance
70 // requires.
71 let mut hasher = std::collections::hash_map::DefaultHasher::new();
72 for param in instance.parameters() {
73 param.name().hash(&mut hasher);
74 param.parameter_value().hash(&mut hasher);
75 }
76 let params_hash = hasher.finish();
77
78 TemplateKey {
79 definition,
80 params_hash,
81 }
82}
83
84/// The actual bucket -> merge-group decision, factored out of `elaborate()`'s
85/// dedup pass into its own `pub` function specifically so it is directly
86/// unit-testable against a FORCED hash collision (NL-013 clause (b)). Every
87/// element of `tagged` carries its own `TemplateKey` — normal callers
88/// (`elaborate()`) always pass the real `template_key_of(..)` result for each
89/// body, but a test may pass two DIFFERENT bodies tagged with the SAME
90/// (possibly fabricated) `TemplateKey` to prove the key alone never causes a
91/// merge. Two entries land in the same output group iff (a) their supplied
92/// `TemplateKey`s are `==` (the filter — decides which pairs are even
93/// compared) AND (b) `Symbol::instance_body_has_same_type` returns `true` for
94/// their bodies (the ground truth — decides whether same-keyed pairs
95/// actually merge). Returns groups as index-sets into `tagged`, in
96/// first-seen order within each group; outer groups are emitted in
97/// first-seen `TemplateKey` order.
98pub fn merge_groups_within_bucket<'d>(
99 tagged: &[(TemplateKey, sv_lang::Symbol<'d>)],
100) -> Vec<Vec<usize>> {
101 // Bucket by TemplateKey (the filter), preserving first-seen order both
102 // for buckets and for entries within a bucket.
103 let mut bucket_order: Vec<TemplateKey> = Vec::new();
104 let mut buckets: HashMap<TemplateKey, Vec<usize>> = HashMap::new();
105 for (index, (key, _symbol)) in tagged.iter().enumerate() {
106 if !buckets.contains_key(key) {
107 bucket_order.push(*key);
108 }
109 buckets.entry(*key).or_default().push(index);
110 }
111
112 let mut groups: Vec<Vec<usize>> = Vec::new();
113 for key in bucket_order {
114 let indices = &buckets[&key];
115 // Within one same-key bucket: greedily grow groups, confirming
116 // every addition pairwise against the group's own canonical
117 // (first) member via `instance_body_has_same_type` — the ground
118 // truth (NL-013 clause (b)). A same-key pair that fails this check
119 // never merges, however the hash landed them in the same bucket.
120 let mut assigned = vec![false; indices.len()];
121 for a in 0..indices.len() {
122 if assigned[a] {
123 continue;
124 }
125 let canonical = &tagged[indices[a]].1;
126 let mut group = vec![indices[a]];
127 assigned[a] = true;
128 for b in (a + 1)..indices.len() {
129 if assigned[b] {
130 continue;
131 }
132 let candidate = &tagged[indices[b]].1;
133 if canonical.instance_body_has_same_type(candidate) {
134 group.push(indices[b]);
135 assigned[b] = true;
136 }
137 }
138 groups.push(group);
139 }
140 }
141 groups
142}
143
144/// Task 3 (NL-015, NL-016): a minimal per-member descriptor used ONLY to
145/// defensively re-verify member-for-member equality when confirming a merge
146/// (Decision 4) — not the full `SignalDecl` (that's Task 5); just (kind,
147/// name) pairs read from `members()`.
148#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct MemberShape {
150 pub kind: sv_lang_kinds::SymbolKind,
151 pub name: String,
152}
153
154/// (Revision, resolving a blocking review finding — Deviation #18.) The
155/// return payload of [`confirm_and_assign_symbol_keys`], split into:
156/// `shapes`, `(SymbolKey, MemberShape)` pairs read off `canonical`'s own
157/// members, in `SymbolKey` order; and `per_body_symbols`, the same lockstep
158/// walk's real `sv_lang::Symbol<'d>` handles, one `Vec` per body in
159/// `[canonical, others[0], others[1], ..]` order, each `Vec` `SymbolKey`-
160/// ordered identically to `shapes` (member `i` of every body's `Vec` is "the
161/// same" logical member, by construction of the lockstep walk).
162pub struct ConfirmedMembers<'d> {
163 pub shapes: Vec<(SymbolKey, MemberShape)>,
164 pub per_body_symbols: Vec<Vec<sv_lang::Symbol<'d>>>,
165}
166
167/// Re-asserts member-for-member (kind, name) equality across every body
168/// already confirmed by `instance_body_has_same_type`, walking `members()`
169/// of both bodies in lockstep, and assigns the SAME dense `SymbolKey`
170/// (`canonical`'s own `members()` position) to matching members across every
171/// body in the group (Decision 4: "the code re-asserts defensively as a
172/// fuzz-testable invariant... a merge that silently drops a mismatch is a
173/// correctness bug, not a warning"). Returns
174/// `Err(ElabError::MergeInvariantViolated)` — never a panic, never a silent
175/// skip of the mismatching member — on any disagreement, even though
176/// `instance_body_has_same_type` already said `true` (defense in depth
177/// against NL-019's still-open verification gap): this function never itself
178/// calls `instance_body_has_same_type` and is fully independent of it, which
179/// is exactly what lets a test simulate "the oracle said yes but our own
180/// re-check disagrees" by calling this function directly with two bodies
181/// that were never actually confirmed by the oracle.
182///
183/// A member-count mismatch (one body has more/fewer members than
184/// `canonical`) is also a `MergeInvariantViolated`, reported at the index one
185/// past the shorter body's last member.
186pub fn confirm_and_assign_symbol_keys<'d>(
187 canonical: sv_lang::Symbol<'d>,
188 others: &[sv_lang::Symbol<'d>],
189) -> Result<ConfirmedMembers<'d>, ElabError> {
190 let canonical_members: Vec<sv_lang::Symbol<'d>> = canonical.members().collect();
191
192 let shapes: Vec<(SymbolKey, MemberShape)> = canonical_members
193 .iter()
194 .enumerate()
195 .map(|(i, m)| {
196 (
197 SymbolKey(i as u32),
198 MemberShape {
199 kind: m.kind(),
200 name: m.name().to_string(),
201 },
202 )
203 })
204 .collect();
205
206 let mut per_body_symbols: Vec<Vec<sv_lang::Symbol<'d>>> = Vec::with_capacity(others.len() + 1);
207 per_body_symbols.push(canonical_members.clone());
208
209 for other in others {
210 let other_members: Vec<sv_lang::Symbol<'d>> = other.members().collect();
211
212 for (i, canonical_member) in canonical_members.iter().enumerate() {
213 let Some(other_member) = other_members.get(i) else {
214 return Err(ElabError::MergeInvariantViolated {
215 canonical_hier_path: canonical.hierarchical_path(),
216 other_hier_path: other.hierarchical_path(),
217 member_index: i,
218 detail: format!(
219 "other body has only {} member(s), canonical has member {} (`{}`, {:?})",
220 other_members.len(),
221 i,
222 canonical_member.name(),
223 canonical_member.kind(),
224 ),
225 });
226 };
227 if other_member.kind() != canonical_member.kind()
228 || other_member.name() != canonical_member.name()
229 {
230 return Err(ElabError::MergeInvariantViolated {
231 canonical_hier_path: canonical.hierarchical_path(),
232 other_hier_path: other.hierarchical_path(),
233 member_index: i,
234 detail: format!(
235 "member {i} mismatch: canonical `{}` ({:?}) vs other `{}` ({:?})",
236 canonical_member.name(),
237 canonical_member.kind(),
238 other_member.name(),
239 other_member.kind(),
240 ),
241 });
242 }
243 }
244
245 if other_members.len() != canonical_members.len() {
246 return Err(ElabError::MergeInvariantViolated {
247 canonical_hier_path: canonical.hierarchical_path(),
248 other_hier_path: other.hierarchical_path(),
249 member_index: canonical_members.len(),
250 detail: format!(
251 "other body has {} member(s), canonical has only {}",
252 other_members.len(),
253 canonical_members.len(),
254 ),
255 });
256 }
257
258 per_body_symbols.push(other_members);
259 }
260
261 Ok(ConfirmedMembers {
262 shapes,
263 per_body_symbols,
264 })
265}