sv_sim_sim/analysis/graph.rs
1//! Task 8 (NL-007, NL-063): the `{net,process}` driver-dependency graph and
2//! the WOCB partitioning pass over it (netlist-analysis.md §C.1;
3//! ARCHITECTURE §2/§6.5).
4//!
5//! **Load-bearing finding, TDD'd against real `sv-lang` output (verified
6//! this task): two anonymous members of the SAME kind in one scope
7//! (e.g. two `assign` statements, or an `assign` and a `ProceduralBlock`)
8//! all share the identical `SymbolId` — the scope's own hierarchical path**
9//! (confirmed empirically: `module m; assign x=a; assign y=b; endmodule`
10//! gives BOTH `ContinuousAssign` members `id() == SymbolId("m")`, and a
11//! module with one `assign` and one anonymous `always_ff` gives the
12//! `ContinuousAssign` AND the `ProceduralBlock` the SAME `id() ==
13//! SymbolId("m")` too). This makes the `SymbolId`-keyed
14//! `netlist::symbol_key_map` table (last-write-wins) UNSAFE for resolving a
15//! `DriverInfo::containing_symbol()`/`AnalyzedProcedure::symbol()` handle
16//! back to its owning member's `SymbolKey` — exactly the identity this
17//! module needs to build a `{net,process}` graph node for "the process a
18//! driver/read belongs to". This module therefore resolves a containing/
19//! procedure symbol to its `SymbolKey` via [`member_position_key`]: a
20//! direct positional scan of `scope.members()` using `Symbol`'s own
21//! pointer-identity `PartialEq` (`ast.rs:22589-22595`, "Symbol identity is
22//! its address"), which distinguishes two colliding-`id()` members
23//! correctly since it never touches `id()` at all. Net/variable resolution
24//! (which never collides — every named net's own `id()` is a real,
25//! unique hierarchical path) keeps using `netlist::symbol_key_map`, reused
26//! rather than re-derived, per this workspace's "never re-derived a second,
27//! independently-reasoned way" posture.
28
29use crate::symid::SymbolUid;
30use std::collections::{HashMap, HashSet};
31
32use crate::netlist::{
33 DriverEntry, InstanceId, InstantiatedSignalId, Netlist, NetlistView, StaticSensitivity,
34 SymbolKey, TemplateId,
35};
36
37use super::{ClockDomainId, RegionId};
38
39/// Whether a graph node is a net (a `Template.signals` member) or a process
40/// (a `ProceduralBlock`/`ContinuousAssign`/sub-`Instance` boundary driver).
41/// Crate-private: external consumers only ever see the resulting
42/// [`Partition::nets`]/[`Partition::processes`] split, never this tag
43/// itself.
44#[derive(Clone, Copy, PartialEq, Eq, Debug)]
45enum NodeKind {
46 Net,
47 Process,
48}
49
50/// One WOCB partition: a set of `{net,process}` graph nodes that must be
51/// evaluated together (netlist-analysis.md's `Partition`). `clock_domain`
52/// is `None` for a purely combinational/mixed region.
53#[derive(Clone, Debug)]
54pub struct Partition {
55 pub id: RegionId,
56 pub processes: Vec<InstantiatedSignalId>,
57 pub nets: Vec<InstantiatedSignalId>,
58 pub clock_domain: Option<ClockDomainId>,
59}
60
61/// The `{net,process}` driver-dependency graph (NL-063): `write_edges()` is
62/// `(process) -write-> (net)` for every real (non-input-port-boundary)
63/// `DriverInfo`; `read_edges()` is `(net) -read-> (process)` for every
64/// `AnalyzedProcedure::read_set()` entry whose net has at least one
65/// internal (non-input-port) driver somewhere in this graph — a pure
66/// top-level input has no driver of its own to synchronize with, so a read
67/// of it is never a partition-membership-relevant dependency (see
68/// [`build_driver_graph`]'s own doc comment for the fixture that pins this
69/// exact filter).
70#[derive(Default)]
71pub struct DriverGraph {
72 write_edges: Vec<(InstantiatedSignalId, InstantiatedSignalId)>,
73 read_edges: Vec<(InstantiatedSignalId, InstantiatedSignalId)>,
74 node_kinds: HashMap<InstantiatedSignalId, NodeKind>,
75 /// (Task 10, NL-117.) Every node this graph knows about (every
76 /// `Template.signals` member and every `Template.processes` member, fanned
77 /// out per instance), in first-seen, fully deterministic order (per-
78 /// `Template` declaration order, then `registration_order()`) — NOT
79 /// filtered to nodes that happen to appear in `write_edges`/`read_edges`.
80 /// [`partition`] seeds its node set from this list before walking edges,
81 /// so a net or process with zero write/read edges (e.g. a bare,
82 /// never-driven/never-read `interconnect` net) still gets its own
83 /// singleton `Partition` rather than silently vanishing from the graph
84 /// entirely — the concrete regression this field exists to fix (a
85 /// declared-but-unconnected signal is not the same thing as "does not
86 /// exist," and dropping it from partitioning outright is itself a form
87 /// of misclassifying it, which NL-117's own acceptance rules out).
88 all_nodes: Vec<InstantiatedSignalId>,
89 /// Every clocked process's own intrinsic clock domain (from its
90 /// `StaticSensitivity::Explicit`'s first edge) — the union-find's
91 /// starting point for domain propagation in [`partition`]. A node not
92 /// present here has no intrinsic domain of its own (combinational, or a
93 /// sub-instance boundary).
94 process_domain: HashMap<InstantiatedSignalId, ClockDomainId>,
95 /// `ClockDomainId(i)` names `domain_registry[i]` — `(clock signal,
96 /// edge)` — interned in first-discovered order (`registration_order()`,
97 /// then each instance's own `Template.processes` order): deterministic,
98 /// never a `HashMap`-iteration-order accident.
99 domain_registry: Vec<(InstantiatedSignalId, crate::state::EdgeKind)>,
100}
101
102impl DriverGraph {
103 /// `(process, net)` pairs — one per real `DriverEntry`.
104 pub fn write_edges(&self) -> &[(InstantiatedSignalId, InstantiatedSignalId)] {
105 &self.write_edges
106 }
107
108 /// `(net, process)` pairs — one per read-set entry (see this module's
109 /// own doc comment for the input-port exclusion).
110 pub fn read_edges(&self) -> &[(InstantiatedSignalId, InstantiatedSignalId)] {
111 &self.read_edges
112 }
113
114 /// The interned clock-signal registry (`analysis::analyze`'s own
115 /// source for `StaticAnalysisResult.clock_domains`) — crate-internal,
116 /// not part of this module's public surface (mirrors
117 /// `Netlist::member_symbol`'s own "internal handle no external
118 /// `NetlistView` consumer needs" reasoning).
119 pub(crate) fn clock_domain_registry(
120 &self,
121 ) -> &[(InstantiatedSignalId, crate::state::EdgeKind)] {
122 &self.domain_registry
123 }
124}
125
126/// Resolves a raw `sv_lang::Symbol` (a `DriverInfo::containing_symbol()` or
127/// an `AnalyzedProcedure::symbol()`) to its `SymbolKey` within `members` —
128/// the flat, position-ordered `scope.members()` list `SymbolKey`
129/// assignment is defined over everywhere else in this crate — via pointer
130/// identity, never `SymbolId` (see this module's own doc comment on why the
131/// latter is unsafe for a `ProceduralBlock`/`ContinuousAssign` container).
132fn member_position_key(
133 members: &[sv_lang::Symbol<'_>],
134 target: sv_lang::Symbol<'_>,
135) -> Option<SymbolKey> {
136 members
137 .iter()
138 .position(|&m| m == target)
139 .map(|i| SymbolKey(i as u32))
140}
141
142/// Resolves every real (non-input-port) driver of `net_symbol` to
143/// `(containing construct's SymbolKey, DriverKind)`. An input-port driver
144/// (`DriverInfo::is_input_port()`) is the parent-scope boundary case —
145/// already modeled by this pass's own synthetic port-connection machinery
146/// (`netlist::elaborate`'s `ContAssignTemplate`/`SubInstanceSlot`), never a
147/// node in THIS per-instance `{net,process}` graph — so it is filtered out
148/// here, the ONE place this decision is made (both [`build_driver_graph`]
149/// and [`populate_driver_kinds`] call this, never a second,
150/// independently-reasoned driver walk). A driver whose containing symbol
151/// cannot be positionally resolved within `members` (a construct this
152/// wave's graph does not model, e.g. a driver inside a different instance
153/// reached through an interface) is defensively skipped too — never a
154/// panic, and not a `sv_lang`/`sv_lang_kinds` kind-match this workspace's
155/// exhaustive-match rule binds (it's a symbol-identity lookup, not a kind
156/// dispatch).
157fn resolve_net_drivers<'d>(
158 slang_analysis: &sv_lang::Analysis<'d>,
159 net_symbol: sv_lang::Symbol<'d>,
160 members: &[sv_lang::Symbol<'d>],
161) -> Vec<(SymbolKey, sv_lang::DriverKind)> {
162 slang_analysis
163 .drivers(net_symbol)
164 .filter(|d| !d.is_input_port())
165 .filter_map(|d| {
166 member_position_key(members, d.containing_symbol()).map(|key| (key, d.kind()))
167 })
168 .collect()
169}
170
171/// **Second load-bearing finding, TDD'd against real `sv-lang` output
172/// (verified this task): `sv_lang::Analysis` (`slang::analysis::
173/// AnalysisManager`) tracks driver/procedure data per CANONICAL instance
174/// only** — confirmed empirically against a fixture with two instances
175/// (`i0`, `i1`) of one shared module: `slang_analysis.procedures(i0_body)
176/// .count() == 3` (i0 happened to be canonical) while
177/// `slang_analysis.procedures(i1_body).count() == 0`, even though Task 2's
178/// own `DisableInstanceCaching` finding already established `i0`'s and
179/// `i1`'s own AST bodies are genuinely distinct `Symbol`s (never pointer-
180/// shared). This is `AnalysisManager.h`'s own documented behavior ("driver
181/// state tracked PER CANONICAL INSTANCE") — a SEPARATE, analysis-layer-only
182/// canonicalization from `DisableInstanceCaching`'s elaboration-layer
183/// pointer-identity guarantee. Consequence: `Analysis::drivers`/
184/// `Analysis::procedures` can only ever be queried against ONE
185/// representative instance per deduped `Template` — never a genuinely
186/// per-instance call — so this function computes the write/read STRUCTURAL
187/// PATTERN (as `(SymbolKey, SymbolKey)` pairs) once per `Template`, against
188/// its own canonical instance (found by [`representative_instance_of`],
189/// which — unlike `populate_driver_kinds`' "any instance is correct,
190/// Decision 4 guarantees identical shape" reasoning — must specifically
191/// locate the one instance `Analysis` actually populated data for, not
192/// merely the lowest `InstanceId`), then FANS that pattern OUT onto every
193/// instance sharing the `Template`, using each instance's own `InstanceId`.
194/// Clock-domain seeding stays genuinely per-instance regardless (NL-076: it
195/// never calls `Analysis` at all — it reads `Template.processes`, already
196/// derived directly from raw `sv_lang` in `elaborate()`, so `i0`'s and
197/// `i1`'s own distinct `InstantiatedSignalId`s for their `clk` port already
198/// differ correctly with no canonicalization pitfall).
199///
200/// **Deviation from the plan's literal "built by zipping
201/// `Analysis::drivers()`/`Analysis::driver_handles()`" wording:** this
202/// implementation uses `Analysis::drivers()` (`DriverInfo`) alone —
203/// `DriverInfo` already exposes both `.kind()` (`DriverEntry.driver_kind`)
204/// and `.containing_symbol()` (the process-identity resolution this graph
205/// needs); `Analysis::driver_handles()`'s `ValueDriver` adds
206/// `.source()`/`.is_unidirectional_port()`/bit-range/override-range data no
207/// test in this task's plan section exercises, so it is not fetched. Adapts
208/// minimally, keeps the specified edge set.
209pub fn build_driver_graph<'d>(
210 netlist: &Netlist<'d>,
211 slang_analysis: &sv_lang::Analysis<'d>,
212) -> DriverGraph {
213 let mut graph = DriverGraph::default();
214 let mut domain_ids: HashMap<InstantiatedSignalId, ClockDomainId> = HashMap::new();
215
216 for template_index in 0..netlist.templates.len() {
217 let tmpl_id = TemplateId(template_index as u32);
218 let template = netlist.template(tmpl_id);
219 // Declaration order (NL-028), never a `HashSet`-iteration-order
220 // accident.
221 let net_key_order: Vec<SymbolKey> = template.signals.iter().map(|s| s.key).collect();
222 let net_key_set: HashSet<SymbolKey> = net_key_order.iter().copied().collect();
223
224 let Some(rep_instance) = representative_instance_of(netlist, slang_analysis, tmpl_id)
225 else {
226 continue;
227 };
228 let Some(body) = netlist.instance_symbol(rep_instance).instance_body() else {
229 continue;
230 };
231 let members: Vec<sv_lang::Symbol<'_>> = crate::netlist::module_items(&body);
232 let scope_keys = crate::netlist::symbol_key_map(body);
233
234 // Structural write pattern (SymbolKey -> SymbolKey), computed ONCE
235 // against the canonical representative.
236 let mut write_pattern: Vec<(SymbolKey, SymbolKey)> = Vec::new();
237 let mut has_internal_driver: HashSet<SymbolKey> = HashSet::new();
238 for &net_key in &net_key_order {
239 let net_symbol = netlist.member_symbol(rep_instance, net_key);
240 let drivers = resolve_net_drivers(slang_analysis, net_symbol, &members);
241 if !drivers.is_empty() {
242 has_internal_driver.insert(net_key);
243 }
244 for (proc_key, _kind) in drivers {
245 write_pattern.push((proc_key, net_key));
246 }
247 }
248
249 // Structural read pattern (SymbolKey -> SymbolKey), same
250 // canonical-representative restriction, filtered to nets with at
251 // least one internal driver (see this module's own doc comment on
252 // `DriverGraph`).
253 let mut read_pattern: Vec<(SymbolKey, SymbolKey)> = Vec::new();
254 for proc in slang_analysis.procedures(body) {
255 let Some(proc_key) = member_position_key(&members, proc.symbol()) else {
256 continue;
257 };
258 for r in proc.read_set() {
259 let Some(&net_key) = scope_keys.get(&r.symbol().uid()) else {
260 continue;
261 };
262 if !net_key_set.contains(&net_key) || !has_internal_driver.contains(&net_key) {
263 continue;
264 }
265 read_pattern.push((net_key, proc_key));
266 }
267 }
268
269 // Fan the structural pattern out onto every instance sharing this
270 // Template, in deterministic `registration_order()`.
271 for instance_id in netlist.registration_order() {
272 if netlist.instance(instance_id).template != tmpl_id {
273 continue;
274 }
275
276 for &net_key in &net_key_order {
277 let net_sig = InstantiatedSignalId {
278 instance: instance_id,
279 member: net_key,
280 };
281 graph.node_kinds.insert(net_sig, NodeKind::Net);
282 graph.all_nodes.push(net_sig);
283 }
284 for proc in &template.processes {
285 let proc_sig = InstantiatedSignalId {
286 instance: instance_id,
287 member: proc.key,
288 };
289 graph
290 .node_kinds
291 .entry(proc_sig)
292 .or_insert(NodeKind::Process);
293 graph.all_nodes.push(proc_sig);
294 }
295 for &(proc_key, net_key) in &write_pattern {
296 let proc_sig = InstantiatedSignalId {
297 instance: instance_id,
298 member: proc_key,
299 };
300 let net_sig = InstantiatedSignalId {
301 instance: instance_id,
302 member: net_key,
303 };
304 graph
305 .node_kinds
306 .entry(proc_sig)
307 .or_insert(NodeKind::Process);
308 graph.write_edges.push((proc_sig, net_sig));
309 }
310 for &(net_key, proc_key) in &read_pattern {
311 let proc_sig = InstantiatedSignalId {
312 instance: instance_id,
313 member: proc_key,
314 };
315 let net_sig = InstantiatedSignalId {
316 instance: instance_id,
317 member: net_key,
318 };
319 graph
320 .node_kinds
321 .entry(proc_sig)
322 .or_insert(NodeKind::Process);
323 graph.read_edges.push((net_sig, proc_sig));
324 }
325
326 // Clock-domain seeding: genuinely per-instance (NL-076), never
327 // Analysis-dependent — see this function's own doc comment.
328 for proc in &template.processes {
329 if let StaticSensitivity::Explicit { edges } = &proc.sensitivity
330 && let Some(&(clk_key, edge_kind)) = edges.first()
331 {
332 let proc_sig = InstantiatedSignalId {
333 instance: instance_id,
334 member: proc.key,
335 };
336 let clk_sig = InstantiatedSignalId {
337 instance: instance_id,
338 member: clk_key,
339 };
340 let domain_id = *domain_ids.entry(clk_sig).or_insert_with(|| {
341 let id = ClockDomainId(graph.domain_registry.len() as u32);
342 graph.domain_registry.push((clk_sig, edge_kind));
343 id
344 });
345 graph.process_domain.insert(proc_sig, domain_id);
346 }
347 }
348 }
349 }
350
351 graph
352}
353
354/// Finds the one instance of `tmpl` that `sv_lang::Analysis` actually
355/// populated driver/procedure data for (see [`build_driver_graph`]'s own
356/// doc comment on why this is NOT simply "the lowest `InstanceId`" in
357/// general — it happens to coincide with DFS-first-visited in the fixtures
358/// this task's tests use, but this function verifies it directly against
359/// `Analysis` itself rather than assuming that correlation holds). A
360/// `Template` with no processes AND no continuous assigns has nothing for
361/// `Analysis` to canonicalize differently across its instances, so the
362/// first one (`registration_order()`) is used without probing. Returns
363/// `None` only if `tmpl` has zero instances, which cannot happen for a
364/// `Netlist` this crate's own `elaborate()` produced (defensive only).
365///
366/// `pub(crate)` (Task 9): `analysis::dpi::collect_call_tags` needs the
367/// SAME canonical-representative resolution to enumerate `Analysis`-backed
368/// call sites (`Analysis::procedures(scope).call_expressions()`) — reused
369/// rather than re-derived, per this workspace's "never a second,
370/// independently-reasoned way" posture.
371pub(crate) fn representative_instance_of<'d>(
372 netlist: &Netlist<'d>,
373 slang_analysis: &sv_lang::Analysis<'d>,
374 tmpl: TemplateId,
375) -> Option<InstanceId> {
376 let template = netlist.template(tmpl);
377 let template_has_analyzable_members =
378 !template.processes.is_empty() || !template.continuous_assigns.is_empty();
379
380 let mut fallback = None;
381 for instance_id in netlist.registration_order() {
382 if netlist.instance(instance_id).template != tmpl {
383 continue;
384 }
385 if fallback.is_none() {
386 fallback = Some(instance_id);
387 }
388 if !template_has_analyzable_members {
389 return Some(instance_id);
390 }
391 let Some(body) = netlist.instance_symbol(instance_id).instance_body() else {
392 continue;
393 };
394 if slang_analysis.procedures(body).count() > 0 {
395 return Some(instance_id);
396 }
397 }
398 fallback
399}
400
401/// Task 5's driver-set backfill (NL-063's classification clause): populates
402/// `SignalDecl.driver_set` for every net across every `Template`, against
403/// ONE representative instance per `Template` (Decision 4 guarantees every
404/// deduped instance has an identical `DriverEntry` shape for the same
405/// member, so classifying via any one of them is correct). Mutates
406/// `netlist` in place — the one mutation this whole pipeline performs on an
407/// already-elaborated `Netlist`.
408///
409/// Implemented as two passes (an immutable read pass collecting every
410/// update, then a mutation pass applying them) rather than one interleaved
411/// pass: `Netlist::instance`/`instance_symbol`/`member_symbol` are
412/// `NetlistView`/inherent methods taking `&self` (the whole struct), so
413/// they cannot be called while `netlist.templates[i]` is borrowed
414/// mutably — a plain, standard borrow-checker-driven split, not a
415/// correctness-relevant design decision.
416pub(crate) fn populate_driver_kinds<'d>(
417 netlist: &mut Netlist<'d>,
418 slang_analysis: &sv_lang::Analysis<'d>,
419) {
420 let mut updates: Vec<(usize, usize, Vec<DriverEntry>)> = Vec::new();
421
422 for template_index in 0..netlist.templates.len() {
423 let tmpl_id = TemplateId(template_index as u32);
424 // Reuses `build_driver_graph`'s own canonical-instance resolution
425 // (never a second, independently-reasoned "pick a representative"
426 // rule) — required for correctness here too: `Analysis::drivers`
427 // only has real data against the SAME canonical instance `Analysis`
428 // itself chose (see `build_driver_graph`'s doc comment).
429 let Some(rep_instance) = representative_instance_of(netlist, slang_analysis, tmpl_id)
430 else {
431 // A `Template` with zero instances cannot happen post-elaborate
432 // (every `Template` is created because at least one instance's
433 // body deduped to it) — defensive only.
434 continue;
435 };
436
437 let Some(body) = netlist.instance_symbol(rep_instance).instance_body() else {
438 continue;
439 };
440 let members: Vec<sv_lang::Symbol<'_>> = crate::netlist::module_items(&body);
441
442 for (signal_index, signal) in netlist.templates[template_index].signals.iter().enumerate() {
443 let net_symbol = netlist.member_symbol(rep_instance, signal.key);
444 let entries: Vec<DriverEntry> =
445 resolve_net_drivers(slang_analysis, net_symbol, &members)
446 .into_iter()
447 .map(|(containing, driver_kind)| DriverEntry {
448 driver_kind,
449 containing,
450 })
451 .collect();
452 if !entries.is_empty() {
453 updates.push((template_index, signal_index, entries));
454 }
455 }
456 }
457
458 for (template_index, signal_index, entries) in updates {
459 netlist.templates[template_index].signals[signal_index].driver_set = entries;
460 }
461}
462
463/// The WOCB partitioning pass (Decision-record union-find): every write
464/// edge always unions process with net (LRM multi-driver atomicity falls
465/// out for free — two processes that both write one net both union with
466/// it, hence transitively with each other, with no special-casing). A read
467/// edge unions net with reader UNLESS both sides already carry a resolved,
468/// DIFFERING clock domain — a genuine cross-domain register-to-register
469/// read, which [`crate::analysis::clock::classify_cdc_edges`] turns into a
470/// `CdcEdge` instead (never a merge, per NL-082). Two same-domain clocked
471/// nodes (NL-076's fixture) also do NOT union on a read alone — finer
472/// partition granularity, still correct (each register independently reads
473/// old values within one clock domain) and not required to merge by
474/// anything this pass's tests assert. Iterated to a fixed point (a merge
475/// can turn a group's domain from `None` to `Some`, unblocking a
476/// previously-skipped union later in the same or a later pass), bounded by
477/// `max_passes`.
478pub fn partition(graph: &DriverGraph, max_passes: u32) -> Vec<Partition> {
479 // Deterministic node discovery: first-seen order across `all_nodes`
480 // (every net/process `build_driver_graph` knows about, declaration- then
481 // registration-ordered — NL-117: this is what guarantees a
482 // never-driven/never-read net, e.g. a bare `interconnect` declaration,
483 // still gets its own singleton `Partition` below instead of silently
484 // never becoming a node at all), THEN write_edges, THEN read_edges (both
485 // already declaration-ordered by `build_driver_graph`) — never a
486 // `HashMap`/`HashSet`-iteration-order accident. The edge scan after
487 // `all_nodes` is defensive-only in the common case (every edge endpoint
488 // is already one of `all_nodes`) but kept so this function still behaves
489 // correctly even against a `DriverGraph` built some other way.
490 let mut nodes: Vec<InstantiatedSignalId> = Vec::new();
491 let mut seen: HashSet<InstantiatedSignalId> = HashSet::new();
492 for &n in &graph.all_nodes {
493 if seen.insert(n) {
494 nodes.push(n);
495 }
496 }
497 for &(a, b) in graph.write_edges.iter().chain(graph.read_edges.iter()) {
498 for n in [a, b] {
499 if seen.insert(n) {
500 nodes.push(n);
501 }
502 }
503 }
504 let index_of: HashMap<InstantiatedSignalId, usize> =
505 nodes.iter().enumerate().map(|(i, &n)| (n, i)).collect();
506
507 let mut parent: Vec<usize> = (0..nodes.len()).collect();
508 let mut domain: Vec<Option<ClockDomainId>> = nodes
509 .iter()
510 .map(|n| graph.process_domain.get(n).copied())
511 .collect();
512
513 fn find(parent: &mut [usize], x: usize) -> usize {
514 if parent[x] != x {
515 parent[x] = find(parent, parent[x]);
516 }
517 parent[x]
518 }
519 fn union(parent: &mut [usize], domain: &mut [Option<ClockDomainId>], a: usize, b: usize) {
520 let ra = find(parent, a);
521 let rb = find(parent, b);
522 if ra == rb {
523 return;
524 }
525 parent[rb] = ra;
526 if domain[ra].is_none() {
527 domain[ra] = domain[rb];
528 }
529 }
530
531 // Phase 1: write edges always union.
532 for &(proc_sig, net_sig) in &graph.write_edges {
533 let (Some(&pi), Some(&ni)) = (index_of.get(&proc_sig), index_of.get(&net_sig)) else {
534 continue;
535 };
536 union(&mut parent, &mut domain, pi, ni);
537 }
538
539 // Phase 2: read edges union to a fixed point, unless both sides already
540 // have a resolved (possibly-differing) clock domain.
541 for _pass in 0..max_passes.max(1) {
542 let mut changed = false;
543 for &(net_sig, proc_sig) in &graph.read_edges {
544 let (Some(&ni), Some(&pi)) = (index_of.get(&net_sig), index_of.get(&proc_sig)) else {
545 continue;
546 };
547 let rn = find(&mut parent, ni);
548 let rp = find(&mut parent, pi);
549 if rn == rp {
550 continue;
551 }
552 if domain[rn].is_none() || domain[rp].is_none() {
553 union(&mut parent, &mut domain, rn, rp);
554 changed = true;
555 }
556 }
557 if !changed {
558 break;
559 }
560 }
561
562 // Assign RegionIds to the final distinct roots, in first-seen node
563 // order, and classify each node into its partition's `.nets`/
564 // `.processes`.
565 let mut region_of_root: HashMap<usize, RegionId> = HashMap::new();
566 let mut partitions: Vec<Partition> = Vec::new();
567 for (i, &sig) in nodes.iter().enumerate() {
568 let root = find(&mut parent, i);
569 let region_id = *region_of_root.entry(root).or_insert_with(|| {
570 let id = RegionId(partitions.len() as u32);
571 partitions.push(Partition {
572 id,
573 processes: Vec::new(),
574 nets: Vec::new(),
575 clock_domain: domain[root],
576 });
577 id
578 });
579 let kind = graph.node_kinds.get(&sig).copied().unwrap_or(NodeKind::Net);
580 let p = &mut partitions[region_id.0 as usize];
581 match kind {
582 NodeKind::Net => p.nets.push(sig),
583 NodeKind::Process => p.processes.push(sig),
584 }
585 }
586
587 partitions
588}