Skip to main content

pedant_graph/analysis/
components.rs

1//! Which nodes reach each other, and the acyclic view that fact produces.
2//!
3//! Two nodes share a component when each reaches the other over selected edges.
4//! Contracting every component leaves a graph with no cycle at all, which is
5//! what a ranker or a renderer needs before it can order anything — so the
6//! condensation is built here, beside the components it contracts, and keeps
7//! every raw edge identity it coalesced.
8//!
9//! Both walks are iterative and indexed by dense identity. Neither reads the raw
10//! edge slice: the selection was applied once, and these read what it produced.
11
12use std::collections::{BTreeMap, BTreeSet};
13
14use crate::id::{GraphEdgeId, GraphId, GraphNodeId, index_of, position};
15
16use super::index::{AnalysisContext, SelectedAdjacency, SelectedIndexes};
17
18/// Marker distinguishing component identities from the graph's own identities.
19#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub struct ComponentIdKind;
21
22/// A component's dense identity within one [`GraphComponents`].
23///
24/// Derived rather than graph-owned: it addresses a result the caller's selection
25/// produced, so it never enters [`crate::CodeGraph`] or its serialized form.
26pub type GraphComponentId = GraphId<ComponentIdKind>;
27
28/// One set of nodes that all reach each other.
29pub struct GraphComponent {
30    id: GraphComponentId,
31    members: Box<[GraphNodeId]>,
32    cyclic: bool,
33}
34
35impl GraphComponent {
36    /// This component's dense identity.
37    pub fn id(&self) -> GraphComponentId {
38        self.id
39    }
40
41    /// Every node it holds, in node-id order.
42    pub fn members(&self) -> &[GraphNodeId] {
43        &self.members
44    }
45
46    /// Whether its members can return to themselves: more than one member, or
47    /// one member that selects an edge to itself.
48    pub fn is_cyclic(&self) -> bool {
49        self.cyclic
50    }
51}
52
53/// Every component of one selected topology, and which one each node belongs
54/// to.
55///
56/// Both views come from one walk, so a component's members and a node's
57/// component can never disagree.
58pub struct GraphComponents {
59    components: Box<[GraphComponent]>,
60    membership: Box<[Option<GraphComponentId>]>,
61}
62
63impl GraphComponents {
64    /// Every component, in smallest-member order, which is also identity order.
65    pub fn components(&self) -> &[GraphComponent] {
66        &self.components
67    }
68
69    /// The component one identity names, if this result holds it.
70    pub fn component(&self, id: GraphComponentId) -> Option<&GraphComponent> {
71        self.components.get(index_of(id.index()))
72    }
73
74    /// The component one node belongs to, if this result holds that node.
75    pub fn component_of(&self, node: GraphNodeId) -> Option<GraphComponentId> {
76        self.membership
77            .get(index_of(node.index()))
78            .copied()
79            .flatten()
80    }
81}
82
83/// Every selected edge between one ordered pair of components.
84///
85/// The raw identities travel with the pair, so contracting a cycle hides no
86/// evidence: a caller resolves each of them through the graph the analysis
87/// borrows.
88pub struct CondensationEdge {
89    source: GraphComponentId,
90    target: GraphComponentId,
91    edges: Box<[GraphEdgeId]>,
92}
93
94impl CondensationEdge {
95    /// The component these edges leave.
96    pub fn source(&self) -> GraphComponentId {
97        self.source
98    }
99
100    /// The component they reach.
101    pub fn target(&self) -> GraphComponentId {
102        self.target
103    }
104
105    /// Every raw edge between that ordered pair, in raw edge-id order.
106    pub fn edges(&self) -> &[GraphEdgeId] {
107        &self.edges
108    }
109}
110
111/// One selected topology with every component contracted to a single node.
112pub struct CondensationGraph {
113    components: GraphComponents,
114    edges: Box<[CondensationEdge]>,
115    order: Box<[GraphComponentId]>,
116}
117
118impl CondensationGraph {
119    /// The components this view contracted.
120    pub fn components(&self) -> &GraphComponents {
121        &self.components
122    }
123
124    /// Every coalesced pair, in source then target order.
125    pub fn edges(&self) -> &[CondensationEdge] {
126        &self.edges
127    }
128
129    /// Every component once, each after the components that reach it.
130    pub fn topological_order(&self) -> &[GraphComponentId] {
131        &self.order
132    }
133}
134
135/// Every strongly connected component of one selected topology.
136///
137/// One forward pass records the order nodes finish in; one backward pass over
138/// that order in reverse collects each component. Both are iterative, so a chain
139/// as long as the admitted node ceiling costs no stack.
140pub(crate) fn discover(analysis: &AnalysisContext<'_>) -> GraphComponents {
141    let indexes = analysis.indexes();
142    let finished = finish_order(indexes);
143    assembled(grouped_backwards(indexes, &finished), indexes)
144}
145
146/// One selected topology with its components contracted and ordered.
147pub(crate) fn condense(analysis: &AnalysisContext<'_>) -> CondensationGraph {
148    let indexes = analysis.indexes();
149    let components = discover(analysis);
150    let edges = coalesced(indexes, &components);
151    let order = ordered(&edges, components.components.len());
152    CondensationGraph {
153        components,
154        edges,
155        order,
156    }
157}
158
159/// Every node in the order a depth-first walk of selected edges finishes it.
160fn finish_order(indexes: &SelectedIndexes) -> Box<[GraphNodeId]> {
161    let width = indexes.node_count();
162    let mut seen: Vec<bool> = vec![false; width];
163    let mut finished: Vec<GraphNodeId> = Vec::with_capacity(width);
164    let mut stack: Vec<(GraphNodeId, usize)> = Vec::new();
165    for at in 0..width {
166        begin(&mut seen, &mut stack, GraphNodeId::new(position(at)));
167        drain(indexes, (&mut seen, &mut stack, &mut finished));
168    }
169    finished.into_boxed_slice()
170}
171
172/// Start one walk at a node, unless an earlier walk already reached it.
173fn begin(seen: &mut [bool], stack: &mut Vec<(GraphNodeId, usize)>, node: GraphNodeId) {
174    if unclaimed(seen, node) {
175        stack.push((node, 0));
176    }
177}
178
179/// Claim one node against a flag table, answering whether this call took it.
180///
181/// The one place a walk's flag is tested and set, so the forward walk and the
182/// backward gathering cannot disagree about what claiming a node means.
183fn unclaimed(flags: &mut [bool], node: GraphNodeId) -> bool {
184    match flags.get_mut(index_of(node.index())) {
185        Some(slot) if !*slot => {
186            *slot = true;
187            true
188        }
189        _ => false,
190    }
191}
192
193/// Run the current walk to completion, recording each node as it finishes.
194fn drain(
195    indexes: &SelectedIndexes,
196    state: (
197        &mut [bool],
198        &mut Vec<(GraphNodeId, usize)>,
199        &mut Vec<GraphNodeId>,
200    ),
201) {
202    let (seen, stack, finished) = state;
203    while let Some(step) = stack.pop() {
204        advance(indexes, (seen, stack, finished), step);
205    }
206}
207
208/// Take one more edge from the node on top of the walk, or finish it.
209fn advance(
210    indexes: &SelectedIndexes,
211    state: (
212        &mut [bool],
213        &mut Vec<(GraphNodeId, usize)>,
214        &mut Vec<GraphNodeId>,
215    ),
216    step: (GraphNodeId, usize),
217) {
218    let (seen, stack, finished) = state;
219    let (node, at) = step;
220    match indexes.outgoing(node).get(at) {
221        Some(entry) => resume(seen, stack, (node, at + 1), entry.node()),
222        None => finished.push(node),
223    }
224}
225
226/// Put one node back on the walk and follow the edge it just took.
227fn resume(
228    seen: &mut [bool],
229    stack: &mut Vec<(GraphNodeId, usize)>,
230    step: (GraphNodeId, usize),
231    reached: GraphNodeId,
232) {
233    stack.push(step);
234    begin(seen, stack, reached);
235}
236
237/// Every component's members, gathered by walking selected edges backwards in
238/// reverse finishing order.
239///
240/// Members arrive in the order the walk claimed them and the groups in the order
241/// the walk found them. The assembly that follows sorts both, so both are still
242/// mutating buffers when they are handed over.
243fn grouped_backwards(indexes: &SelectedIndexes, finished: &[GraphNodeId]) -> Vec<Vec<GraphNodeId>> {
244    let mut taken: Vec<bool> = vec![false; indexes.node_count()];
245    let mut found: Vec<Vec<GraphNodeId>> = Vec::new();
246    for node in finished.iter().rev() {
247        let members = gathered(indexes, &mut taken, *node);
248        match members.is_empty() {
249            true => (),
250            false => found.push(members),
251        }
252    }
253    found
254}
255
256/// Every still-unclaimed node that reaches one root over selected edges.
257fn gathered(indexes: &SelectedIndexes, taken: &mut [bool], root: GraphNodeId) -> Vec<GraphNodeId> {
258    let mut members: Vec<GraphNodeId> = Vec::new();
259    let mut stack: Vec<GraphNodeId> = Vec::new();
260    claim(taken, &mut stack, root);
261    while let Some(node) = stack.pop() {
262        members.push(node);
263        for entry in indexes.incoming(node) {
264            claim(taken, &mut stack, entry.node());
265        }
266    }
267    members
268}
269
270/// Claim one node for the component being gathered, unless it is already
271/// claimed.
272fn claim(taken: &mut [bool], stack: &mut Vec<GraphNodeId>, node: GraphNodeId) {
273    if unclaimed(taken, node) {
274        stack.push(node);
275    }
276}
277
278/// One dense component table, ordered by smallest member.
279fn assembled(grouped: Vec<Vec<GraphNodeId>>, indexes: &SelectedIndexes) -> GraphComponents {
280    let mut ordered = grouped;
281    for members in &mut ordered {
282        members.sort_unstable();
283    }
284    ordered.sort_unstable_by_key(|members| members.first().copied());
285    let components: Box<[GraphComponent]> = ordered
286        .into_iter()
287        .enumerate()
288        .map(|(at, members)| built(at, members, indexes))
289        .collect();
290    let membership = placed(&components, indexes.node_count());
291    GraphComponents {
292        components,
293        membership,
294    }
295}
296
297/// One component record, identified by its position in that order.
298///
299/// The gathered list is taken by value and sealed in place, so no two member
300/// lists of one result are ever live at once.
301fn built(at: usize, members: Vec<GraphNodeId>, indexes: &SelectedIndexes) -> GraphComponent {
302    GraphComponent {
303        id: GraphComponentId::new(position(at)),
304        cyclic: cyclic(&members, indexes),
305        members: members.into_boxed_slice(),
306    }
307}
308
309/// Whether one component's members can return to themselves.
310///
311/// More than one member is already a cycle, because each reaches the others. A
312/// lone member is cyclic only when it selects an edge back to itself.
313fn cyclic(members: &[GraphNodeId], indexes: &SelectedIndexes) -> bool {
314    match members {
315        [only] => indexes
316            .outgoing(*only)
317            .iter()
318            .any(|entry| entry.node() == *only),
319        _ => members.len() > 1,
320    }
321}
322
323/// Each node's component, indexed by node.
324///
325/// Filled from nothing rather than from a component identity, so a node no walk
326/// claimed answers with no component instead of silently answering with the
327/// first one.
328fn placed(components: &[GraphComponent], width: usize) -> Box<[Option<GraphComponentId>]> {
329    let mut table: Vec<Option<GraphComponentId>> = vec![None; width];
330    for component in components {
331        for member in component.members.iter() {
332            record(&mut table, *member, component.id);
333        }
334    }
335    table.into_boxed_slice()
336}
337
338/// Record which component one node belongs to.
339fn record(table: &mut [Option<GraphComponentId>], node: GraphNodeId, component: GraphComponentId) {
340    if let Some(slot) = table.get_mut(index_of(node.index())) {
341        *slot = Some(component);
342    }
343}
344
345/// Every selected edge that crosses a component boundary, coalesced by ordered
346/// component pair.
347fn coalesced(indexes: &SelectedIndexes, components: &GraphComponents) -> Box<[CondensationEdge]> {
348    let mut crossing: Crossings = BTreeMap::new();
349    for at in 0..indexes.node_count() {
350        cross(
351            indexes,
352            components,
353            &mut crossing,
354            GraphNodeId::new(position(at)),
355        );
356    }
357    crossing.into_iter().map(joined).collect()
358}
359
360/// Every raw edge found between one ordered pair of components.
361type Crossings = BTreeMap<(GraphComponentId, GraphComponentId), Vec<GraphEdgeId>>;
362
363/// Record every selected edge leaving one node for another component.
364fn cross(
365    indexes: &SelectedIndexes,
366    components: &GraphComponents,
367    crossing: &mut Crossings,
368    node: GraphNodeId,
369) {
370    let source = match components.component_of(node) {
371        Some(found) => found,
372        None => return,
373    };
374    for entry in indexes.outgoing(node) {
375        leaving(components, crossing, (source, *entry));
376    }
377}
378
379/// Record one selected edge, unless both of its ends sit in one component.
380fn leaving(
381    components: &GraphComponents,
382    crossing: &mut Crossings,
383    step: (GraphComponentId, SelectedAdjacency),
384) {
385    let (source, entry) = step;
386    let target = match components.component_of(entry.node()) {
387        Some(found) => found,
388        None => return,
389    };
390    match source == target {
391        true => (),
392        false => crossing
393            .entry((source, target))
394            .or_default()
395            .push(entry.edge()),
396    }
397}
398
399/// One coalesced pair, with its raw edges in raw edge-id order.
400fn joined(crossing: ((GraphComponentId, GraphComponentId), Vec<GraphEdgeId>)) -> CondensationEdge {
401    let ((source, target), mut edges) = crossing;
402    edges.sort_unstable();
403    CondensationEdge {
404        source,
405        target,
406        edges: edges.into_boxed_slice(),
407    }
408}
409
410/// The deterministic topological order of one condensation.
411///
412/// Contracting whole components leaves no cycle, so every component eventually
413/// becomes ready and the order states each of them once. Components that become
414/// ready together are consumed by identity.
415fn ordered(edges: &[CondensationEdge], count: usize) -> Box<[GraphComponentId]> {
416    let mut waiting: Vec<usize> = vec![0; count];
417    for edge in edges {
418        bump(&mut waiting, edge.target);
419    }
420    let mut ready: BTreeSet<GraphComponentId> = (0..count)
421        .map(|at| GraphComponentId::new(position(at)))
422        .filter(|component| waiting.get(index_of(component.index())) == Some(&0))
423        .collect();
424    let mut consumed: Vec<GraphComponentId> = Vec::with_capacity(count);
425    while let Some(least) = ready.pop_first() {
426        consumed.push(least);
427        release(edges, least, (&mut waiting, &mut ready));
428    }
429    consumed.into_boxed_slice()
430}
431
432/// Count one more coalesced edge arriving at a component.
433fn bump(waiting: &mut [usize], component: GraphComponentId) {
434    if let Some(slot) = waiting.get_mut(index_of(component.index())) {
435        *slot += 1;
436    }
437}
438
439/// Consume every edge leaving one ordered component, admitting whatever that
440/// leaves ready.
441fn release(
442    edges: &[CondensationEdge],
443    consumed: GraphComponentId,
444    state: (&mut [usize], &mut BTreeSet<GraphComponentId>),
445) {
446    let (waiting, ready) = state;
447    ready.extend(
448        leaving_pairs(edges, consumed)
449            .iter()
450            .filter_map(|edge| arrived(waiting, edge.target)),
451    );
452}
453
454/// The coalesced pairs leaving one component.
455///
456/// `coalesced` states each pair once, keyed by an ordered component pair, so the
457/// slice is sorted by source and one component's pairs are a contiguous run.
458/// Bounding that run costs two binary searches rather than a scan of every pair
459/// per ordered component, which is what keeps the whole ordering linear in the
460/// pairs it consumes.
461fn leaving_pairs(edges: &[CondensationEdge], source: GraphComponentId) -> &[CondensationEdge] {
462    let start = edges.partition_point(|edge| edge.source < source);
463    let end = edges.partition_point(|edge| edge.source <= source);
464    &edges[start..end]
465}
466
467/// Record one arriving edge as consumed, answering with its component when the
468/// last one is.
469///
470/// A component already released states no further arrival to consume, so the
471/// decrement is checked: a saturating one would answer with a component a second
472/// time and state it twice in the order.
473fn arrived(waiting: &mut [usize], component: GraphComponentId) -> Option<GraphComponentId> {
474    let slot = waiting.get_mut(index_of(component.index()))?;
475    *slot = slot.checked_sub(1)?;
476    match *slot {
477        0 => Some(component),
478        _ => None,
479    }
480}