sv_sim_sim/process/dedup.rs
1//! Task S5 (`docs/superpowers/plans/2026-09-26-wave-A2-slice.md`): the
2//! process TEMPLATE dedup key (PR-028, PR-029, PR-030) and the
3//! `ProcessInstanceId` registration-order tie-break (PR-075, PR-076,
4//! PR-077).
5//!
6//! **PR-028's core claim** ("process template dedup must key off slang's own
7//! `InstanceBodySymbol` canonicalization ... compiling one kernel per
8//! canonical process shape and sharing it across every instance") is
9//! implemented here as a direct `(definition, resolved params)` equality key
10//! ([`ProcessTemplateKey`]), not a from-scratch structural-equality pass over
11//! the AST: `definition` is `sv_lang`'s own stable [`crate::symid::SymbolId`] for
12//! the shared `Definition` symbol (the same symbol every structurally-
13//! identical instance's body resolves back to via
14//! `instance_body_definition()`), and `params` is the instance's own
15//! elaborated, already-folded [`sv_lang::ConstantValue`] list. Two instances
16//! with an identical `(definition, params)` pair are, by construction,
17//! instances of the same module with the same resolved parameter values --
18//! exactly `PR-028`'s dedup criterion -- with no separate call to
19//! `Symbol::instance_body_has_same_type` needed at this layer (unlike
20//! `netlist::dedup`'s own `TemplateKey`, which buckets on a lossy `u64` hash
21//! and so *must* re-verify with the real oracle before merging; this key
22//! carries the real, structured parameter values, so key equality already
23//! *is* the ground truth).
24//!
25//! **Deviation from the plan's illustrative signature** (`fn
26//! template_key_of(instance_body: &Symbol<'_>) -> ProcessTemplateKey`, taking
27//! only the instance `body`): verified against pinned `sv-lang = "=0.1.9"`
28//! that `Symbol::parameters()`'s own doc/SAFETY comment ("count is 0 for
29//! non-instances") means it is populated only when called on an
30//! `Instance`-kind symbol, never on that instance's `InstanceBody` -- the
31//! exact same constraint `netlist::dedup::template_key_of` already
32//! documents and works around. [`template_key_of`] therefore takes both the
33//! owning `Instance` symbol (for `parameters()`) and its `InstanceBody`
34//! (for `instance_body_definition()`), by value (`Symbol` is `Copy` and
35//! pointer-sized), mirroring that precedent exactly.
36//!
37//! **Deviation: `ProcessTemplateKey`'s `Hash`/`Eq` are hand-written, not
38//! derived.** `sv_lang::ConstantValue` derives only `Clone, Debug, PartialEq`
39//! (it is `#[non_exhaustive]` and its `Real` variant is a bare `f64`, which
40//! has no `Eq`; its `Integer` variant wraps `SVInt`, which derives `Eq` but
41//! not `Hash`), so `#[derive(Eq, Hash)]` on a struct embedding
42//! `Vec<ConstantValue>` does not compile. [`hash_constant_value`] below
43//! provides a `Hash` implementation kept manually consistent with
44//! `ConstantValue`'s derived `PartialEq` (structurally recursing the same
45//! way, normalizing `-0.0`/`0.0` to the same hash since `PartialEq` already
46//! treats them as equal), and `Eq` is asserted as a marker trait on top of
47//! that same `PartialEq`. The one honest gap this leaves (documented, not
48//! silently swept under a `#[derive]`): a `real`-valued module parameter
49//! equal to `NaN` is `PartialEq`-unequal to itself, so `Eq`'s reflexivity
50//! technically does not hold for that one value class. This can only ever
51//! cost a missed dedup opportunity (two `NaN`-parameterized instances
52//! compiling their own, functionally-identical kernels instead of sharing
53//! one) -- never a wrong simulation result, since every instance still gets
54//! its own correct `ActivationRecord`/state-arena storage regardless of
55//! kernel sharing (PR-030) -- and the A2 slice's construct set has no `real`
56//! parameters at all, so no slice fixture can even reach this path.
57use crate::symid::SymbolUid;
58use std::hash::{Hash, Hasher};
59
60use crate::symid::SymbolId;
61use sv_lang::{ConstantValue, Symbol};
62
63use crate::netlist::InstanceId;
64use crate::process::ProcessInstanceId;
65
66/// PR-029: `struct ProcessTemplateKey{definition: SymbolId, params:
67/// Vec<ConstantValue>}` -- a hashable dedup key built from
68/// `Symbol::definition()`+`parameters()` (see this module's doc comment for
69/// the `instance_body_definition()` deviation), used for O(1) amortized
70/// template lookup (an ordinary `HashMap<ProcessTemplateKey, _>` in the
71/// caller, e.g. codegen's future per-template kernel cache).
72#[derive(Clone, Debug)]
73pub struct ProcessTemplateKey {
74 pub definition: SymbolId,
75 pub params: Vec<ConstantValue>,
76}
77
78impl PartialEq for ProcessTemplateKey {
79 fn eq(&self, other: &Self) -> bool {
80 self.definition == other.definition && self.params == other.params
81 }
82}
83
84/// See this module's doc comment ("`Hash`/`Eq` are hand-written, not
85/// derived") for exactly why this is a plain marker `impl` over the
86/// `PartialEq` above rather than `#[derive(Eq)]`.
87impl Eq for ProcessTemplateKey {}
88
89impl Hash for ProcessTemplateKey {
90 fn hash<H: Hasher>(&self, state: &mut H) {
91 self.definition.hash(state);
92 self.params.len().hash(state);
93 for p in &self.params {
94 hash_constant_value(p, state);
95 }
96 }
97}
98
99/// A hand-written `Hash` for `sv_lang::ConstantValue`, kept structurally in
100/// step with its own derived `PartialEq` (see this module's doc comment).
101/// Each variant is tagged with a distinct discriminant byte before its
102/// payload is hashed, so e.g. `Integer(0)` and `String("0")` never collide
103/// on payload alone. `Integer`/`Real` are hashed via a stable textual/bit
104/// form rather than field-by-field (`SVInt` has no public field accessors
105/// besides its already-`Display`-backed `to_string()`, and does not itself
106/// implement `Hash`).
107fn hash_constant_value<H: Hasher>(cv: &ConstantValue, state: &mut H) {
108 match cv {
109 ConstantValue::Integer(i) => {
110 0u8.hash(state);
111 i.to_string().hash(state);
112 }
113 ConstantValue::Real(r) => {
114 1u8.hash(state);
115 // Normalize -0.0 to 0.0's bit pattern: `PartialEq` (`==`) already
116 // treats them as equal, and `Hash` must agree with `Eq` for
117 // every pair its own `PartialEq` calls equal.
118 let bits = if *r == 0.0 {
119 0.0f64.to_bits()
120 } else {
121 r.to_bits()
122 };
123 bits.hash(state);
124 }
125 ConstantValue::String(s) => {
126 2u8.hash(state);
127 s.hash(state);
128 }
129 ConstantValue::Null => 3u8.hash(state),
130 ConstantValue::Unbounded => 4u8.hash(state),
131 ConstantValue::Array(elems) => {
132 5u8.hash(state);
133 elems.len().hash(state);
134 for e in elems {
135 hash_constant_value(e, state);
136 }
137 }
138 ConstantValue::Map(s, is_empty) => {
139 6u8.hash(state);
140 s.hash(state);
141 is_empty.hash(state);
142 }
143 ConstantValue::Union(s) => {
144 7u8.hash(state);
145 s.hash(state);
146 }
147 ConstantValue::TaggedUnion {
148 member,
149 value,
150 text,
151 } => {
152 9u8.hash(state);
153 member.hash(state);
154 text.hash(state);
155 hash_constant_value(value, state);
156 }
157 ConstantValue::Bad => 8u8.hash(state),
158 // `ConstantValue` is `#[non_exhaustive]` upstream -- this is not one
159 // of this workspace's own pinned "kind" enums the ARCHITECTURE ยง7
160 // item 9 exhaustive-match discipline targets (those are
161 // `StatementKind`/`ExpressionKind`/`TimingControlKind`/the
162 // procedure-kind enum, each guarded by its own `::COUNT` pin in
163 // `determinism::kind_guard`); a plain value type has no such count
164 // to pin against. A future `sv-lang` variant still gets a stable
165 // (if coarser) hash via its `Debug` text rather than silently
166 // colliding with an existing tag or panicking.
167 other => {
168 9u8.hash(state);
169 format!("{other:?}").hash(state);
170 }
171 }
172}
173
174/// PR-029: builds a [`ProcessTemplateKey`] from an instance's owning
175/// `Instance` symbol (for `parameters()`) and its `InstanceBody` (for
176/// `instance_body_definition()`) -- see this module's doc comment for why
177/// both are needed. `params` uses each parameter's elaborated
178/// [`ConstantValue`] (`Symbol::constant_value()`, forced during elaboration,
179/// full-width, `defparam`-aware) rather than the printed-text
180/// `parameter_value()` netlist's own `TemplateKey` uses, per this task's
181/// literal `Vec<ConstantValue>` field type. A parameter whose constant value
182/// failed to fold (`None`) is recorded as `ConstantValue::Bad` -- a real,
183/// distinguishable member of the key rather than being silently dropped
184/// (which would shift every later parameter's position and could spuriously
185/// equate two instances with a different number of foldable parameters).
186pub fn template_key_of<'d>(instance: Symbol<'d>, body: Symbol<'d>) -> ProcessTemplateKey {
187 let definition = body
188 .instance_body_definition()
189 .map(|def| def.uid())
190 // Defensive fallback, not a silent success path: a well-formed
191 // elaborated `InstanceBody` always has a `Definition`
192 // (`instance_body_definition()` only returns `None` for a
193 // malformed/uninstantiated body, which the A2-slice's `discover_
194 // processes`/module-discovery layer never hands this function in
195 // the first place). Falling back to the body's OWN hierarchical
196 // path keeps the key well-defined and deterministic rather than
197 // panicking, while guaranteeing such a body never spuriously
198 // dedups with any other instance's key (every body's own
199 // hierarchical path is unique).
200 .unwrap_or_else(|| body.uid());
201
202 let params = instance
203 .parameters()
204 .map(|p| p.constant_value().unwrap_or(ConstantValue::Bad))
205 .collect();
206
207 ProcessTemplateKey { definition, params }
208}
209
210/// PR-075/076/077: assigns `ProcessInstanceId(0), ProcessInstanceId(1), ...`
211/// in exactly `order`'s sequence. `order` is meant to be
212/// `netlist::NetlistView::registration_order()`'s own DFS-order output --
213/// this function performs no traversal of its own (see
214/// `no_second_preorder_walk_of_design_exists_in_process_dedup_rs` in
215/// `tests/process_dedup.rs`): PR-076's reconciliation note is explicit that
216/// `registration_order()` (elaboration's own deterministic pre-order DFS
217/// over the design's top-level instance list) directly **is** the tie-break
218/// order, so a
219/// second, independently-derived pre-order walk here would just be a second
220/// place the "canonical instance order" could silently disagree with the
221/// first -- exactly the bug class PR-076's `superseded_by` note names and
222/// this function is deliberately shaped to make impossible.
223pub fn assign_process_instance_ids(
224 order: &[InstanceId],
225) -> std::collections::HashMap<InstanceId, ProcessInstanceId> {
226 order
227 .iter()
228 .enumerate()
229 .map(|(i, id)| (*id, ProcessInstanceId(i as u64)))
230 .collect()
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 /// PR-075/077, at the pure-function level (no `sv-lang` parse needed):
238 /// ids follow `order`'s own sequence, not `InstanceId`'s numeric value.
239 /// The full `netlist::elaborate`-backed determinism proof lives in
240 /// `tests/process_dedup.rs`.
241 #[test]
242 fn assign_process_instance_ids_follows_input_order_not_instance_id_value() {
243 let order = vec![InstanceId(7), InstanceId(3), InstanceId(9)];
244 let ids = assign_process_instance_ids(&order);
245
246 assert_eq!(ids.len(), 3);
247 assert_eq!(ids[&InstanceId(7)], ProcessInstanceId(0));
248 assert_eq!(ids[&InstanceId(3)], ProcessInstanceId(1));
249 assert_eq!(ids[&InstanceId(9)], ProcessInstanceId(2));
250 }
251}