1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
//! Task 31 (`abi` track's wave-A implementation plan, XC-045 **partial**
//! closure — see `docs/CONTRACTS.md`'s "Partial closures (wave A)" entry for
//! the real scoping): a generic, `sv-lang`-free hash-then-verify dedup
//! algorithm (ARCHITECTURE §7 item 7). `Design::compile()` forces
//! `DisableInstanceCaching`, so `sv-lang` never pointer-shares two instance
//! bodies, even textually identical ones — dedup must instead bucket
//! candidates by a cheap hash (the filter) and confirm every same-hash pair
//! pairwise via a ground-truth `same_type` check (the verify) before
//! merging, never trusting hash equality alone.
//!
//! This module implements that algorithm generically, over a caller-supplied
//! `T`/hash/verify triple, so it needs no `sv-lang` dependency at all and
//! stays available under `cargo check -p vj-sim --no-default-features`
//! (`docs/CONTRACTS.md` D4) — unlike [`super::dedup`], which is
//! `#[cfg(feature = "frontend")]`-only and concrete over real
//! `sv_lang::Symbol` instance bodies.
//!
//! **Deviation from the wave-A `abi` plan's literal file location**
//! (`netlist/dedup.rs`, described there as a "new — additive module"): by
//! the time this task ran, `netlist/dedup.rs` already held real,
//! `sv-lang`-specific production code the `netlist` track landed
//! independently and earlier (`netlist` Task 2/3 —
//! `TemplateKey`/`merge_groups_within_bucket`/`confirm_and_assign_symbol_keys`,
//! `#[cfg(feature = "frontend")]`-gated, `git log` shows commits `5115693`
//! "netlist task 2" and `f53bf9f` "netlist task 3" already on this branch).
//! That module is owned by the `netlist` track (`docs/CONTRACTS.md`:
//! "`vj-sim::netlist` (owner: **netlist**)"). Landing this task's
//! generic algorithm there would either silently overwrite the `netlist`
//! track's real, tested production code, or force this sv-lang-free
//! algorithm behind the `frontend` feature it explicitly does not need
//! (every item in that file is `#[cfg(feature = "frontend")]` via
//! `netlist/mod.rs`'s `mod dedup;`). Instead this task lands its algorithm
//! as its own unconditional sibling module, `netlist::dedup_algorithm`
//! (this file), added to `netlist/mod.rs` outside the `frontend` gate —
//! the exact "append an unconditional item to `netlist/mod.rs` for a
//! cross-cutting `abi`-track concern" pattern Task 30 already used for
//! XC-039's `#[cfg(test)] mod tests` block there. The `netlist` track's own
//! `merge_groups_within_bucket` (`dedup.rs`) already independently
//! implements this exact same algorithm shape, concretely, against real
//! `sv_lang::Symbol` bodies (and — per `netlist/dedup_verification.md` —
//! has already been verified live against several real parameterized SV
//! fixtures); this module is the generic, reusable building block the
//! ledger row asks for, kept as a separate, directly unit-testable surface
//! rather than duplicated into or grafted onto the `netlist` track's file.
use HashMap;
use Hash;
/// Groups `items` into merge-groups via ARCHITECTURE §7 item 7's
/// hash-then-verify algorithm: bucket by `hash_of` (the candidate filter —
/// decides which pairs are even compared), then within each bucket, greedily
/// grow groups by confirming every candidate pairwise, against the group's
/// own first ("canonical") member, via `same_type` (the ground truth —
/// decides whether a same-hash pair actually merges). A hash collision
/// without a matching `same_type` result never merges two items, however
/// the hash landed them in the same bucket — the exact property XC-045's
/// acceptance names.
///
/// Returns groups of item-indices into `items`, each group dedup-able into
/// one compiled `Template`; both outer groups (by first-seen bucket order)
/// and indices within a group (first-seen order) are deterministic given a
/// deterministic `hash_of`/`same_type`.
///
/// Complexity is **not** a universal `O(total items)` bound — that claim
/// was checked in an earlier revision of this doc comment and was false; see
/// below for the verified bound.
///
/// Calls `same_type` at most `items.len() - 1` times in total **only in the
/// best case**, where every same-hash item within a bucket ultimately merges
/// into a single group: each successful merge immediately marks the
/// compared item `assigned`, removing it from every later comparison, so the
/// whole bucket costs one call per non-first item, i.e. `O(total items)`.
///
/// That bound does **not** hold when same-hash items fail to merge. A failed
/// `same_type` call leaves both items unassigned, so a candidate that never
/// matches any earlier canonical becomes a new canonical itself and is then
/// compared against by every later item in the bucket. In the fully
/// adversarial case — a bucket of `k` items whose `same_type` always returns
/// `false` (a hash collision between `k` genuinely different types, exactly
/// the case hash-then-verify dedup exists to reject) — every one of the
/// `k*(k-1)/2` pairs within the bucket gets exactly one `same_type` call,
/// i.e. `O(bucket_size^2)`, not `O(bucket_size)`. See
/// `worst_case_all_distinct_same_hash_is_quadratic_in_same_type_calls` below,
/// which pins this real behavior with a call counter (N=100 => 4950 calls).
///
/// So: this function calls `same_type` `O(total items)` times only when most
/// same-hash items in a bucket end up merging into few groups; when hash
/// collisions occur between items that mostly do *not* match each other, it
/// degrades to `O(bucket_size^2)` `same_type` calls per such bucket.
/// ARCHITECTURE §7 item 7's `O(total instances)` bound is about
/// *elaboration* walking every instance before dedup runs at all — it does
/// not by itself imply, and this function does not provide, a worst-case
/// linear bound on `same_type` calls when same-hash buckets contain many
/// mutually non-matching items.