Skip to main content

vj_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}