Skip to main content

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}