Skip to main content

pedant_graph/analysis/
index.rs

1//! The borrowed analysis view and the indexes every query reads.
2//!
3//! One selection is applied here and nowhere else: this module is the only
4//! reader of the raw edge slice, so a neighborhood, a path, a subgraph, and
5//! every later metric answer over the same admitted topology. Construction
6//! proves both ceilings before it allocates anything indexed by them, and
7//! answers with no analysis at all when either refuses.
8//!
9//! The public query methods sit beside the view they read rather than beside
10//! the algorithms that answer them: a type's inherent API belongs in one file,
11//! and each entry point here is one delegation to the module that owns the
12//! work.
13//!
14//! The indexes and the partition are shared immutable values, and every
15//! algorithm reads them through one borrowed [`AnalysisContext`]. A borrowed
16//! view and an owned cached handle therefore hand the same concrete state to
17//! the same owners: neither is a second analysis, and neither can apply a
18//! selection the other did not.
19
20use std::sync::Arc;
21
22use crate::edge::GraphEdge;
23use crate::graph::CodeGraph;
24use crate::id::{GraphEdgeId, GraphNodeId, index_of, position};
25
26use super::betweenness::{self, BetweennessCentrality};
27use super::components::{self, CondensationGraph, GraphComponents};
28use super::degree::{self, DegreeCentrality};
29use super::divergence::{self, GraphDivergence};
30use super::error::GraphAnalysisError;
31use super::layout::{self, LayoutAssistMetadata};
32use super::limits::GraphAnalysisLimits;
33use super::partition::{self, DeclaredModulePartition};
34use super::selection::GraphEdgeSelection;
35use super::traversal::{self, GraphDirection, GraphNeighbor, GraphPath, GraphSubgraph};
36
37/// One selected edge, from the node whose adjacency list holds it.
38///
39/// The other endpoint travels with the identity, so a walk never reads the raw
40/// edge slice back to ask where an edge goes.
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub(crate) struct SelectedAdjacency {
43    edge: GraphEdgeId,
44    node: GraphNodeId,
45}
46
47impl SelectedAdjacency {
48    /// The selected edge's own identity.
49    pub(crate) fn edge(self) -> GraphEdgeId {
50        self.edge
51    }
52
53    /// The endpoint this entry reaches.
54    pub(crate) fn node(self) -> GraphNodeId {
55        self.node
56    }
57}
58
59/// The immutable indexes one admitted selection produces.
60pub(crate) struct SelectedIndexes {
61    outgoing: Box<[Box<[SelectedAdjacency]>]>,
62    incoming: Box<[Box<[SelectedAdjacency]>]>,
63    parents: Box<[Option<GraphNodeId>]>,
64    edges: u32,
65}
66
67impl SelectedIndexes {
68    /// Index `graph` under `selection`, for the node and selected-edge counts
69    /// both ceilings already admitted.
70    ///
71    /// Each adjacency list is filled in raw edge-id order, so every answer read
72    /// from it is in that order before any output sorting. The admitted edge
73    /// count travels with the indexes, so a later metric bounds its own work
74    /// against what was indexed rather than counting the graph again.
75    fn build(graph: &CodeGraph, selection: GraphEdgeSelection, admitted: (u32, u32)) -> Self {
76        let (nodes, edges) = admitted;
77        let width = index_of(nodes);
78        let mut outgoing: Vec<Vec<SelectedAdjacency>> = vec![Vec::new(); width];
79        let mut incoming: Vec<Vec<SelectedAdjacency>> = vec![Vec::new(); width];
80        let mut parents: Vec<Option<GraphNodeId>> = vec![None; width];
81        for edge in selected(graph, selection) {
82            append(
83                &mut outgoing,
84                edge.source(),
85                entry(edge.id(), edge.target()),
86            );
87            append(
88                &mut incoming,
89                edge.target(),
90                entry(edge.id(), edge.source()),
91            );
92        }
93        for edge in graph.containment() {
94            claim(&mut parents, edge.child(), edge.parent());
95        }
96        Self {
97            outgoing: sealed(outgoing),
98            incoming: sealed(incoming),
99            parents: parents.into_boxed_slice(),
100            edges,
101        }
102    }
103
104    /// The selected edges leaving one node, in raw edge-id order.
105    pub(crate) fn outgoing(&self, node: GraphNodeId) -> &[SelectedAdjacency] {
106        borrowed(&self.outgoing, node)
107    }
108
109    /// The selected edges entering one node, in raw edge-id order.
110    pub(crate) fn incoming(&self, node: GraphNodeId) -> &[SelectedAdjacency] {
111        borrowed(&self.incoming, node)
112    }
113
114    /// The containment parent one node is owned by, if it has one.
115    pub(crate) fn parent(&self, node: GraphNodeId) -> Option<GraphNodeId> {
116        self.parents.get(index_of(node.index())).copied().flatten()
117    }
118
119    /// How many nodes these indexes address.
120    pub(crate) fn node_count(&self) -> usize {
121        self.parents.len()
122    }
123
124    /// How many edges the selection admitted into them.
125    pub(crate) fn selected_edges(&self) -> u32 {
126        self.edges
127    }
128
129    /// Whether these indexes address one node at all.
130    pub(crate) fn holds(&self, node: GraphNodeId) -> bool {
131        index_of(node.index()) < self.parents.len()
132    }
133}
134
135/// The immutable state one admitted selection produces, held by shared
136/// ownership.
137///
138/// A borrowed analysis and a cached handle both hold this value, so requesting
139/// a second view of one selection clones two pointers rather than reading the
140/// edge slice again. Nothing here is mutable once built, which is what makes
141/// the sharing safe to hand across threads.
142#[derive(Clone)]
143pub(crate) struct SharedAnalysis {
144    indexes: Arc<SelectedIndexes>,
145    partition: Arc<DeclaredModulePartition>,
146}
147
148impl SharedAnalysis {
149    /// Index `graph` under `selection`, proving both stated ceilings first.
150    pub(crate) fn indexed(
151        graph: &CodeGraph,
152        selection: GraphEdgeSelection,
153        limits: GraphAnalysisLimits,
154    ) -> Result<Self, GraphAnalysisError> {
155        let counts = admitted_counts(graph, selection, limits)?;
156        Self::from_admitted(graph, selection, counts)
157    }
158
159    /// Index `graph` for node and selected-edge counts a ceiling already
160    /// admitted.
161    ///
162    /// The counts travel in rather than being taken again, so a cached lookup
163    /// proves the caller's current ceilings once and the miss behind it indexes
164    /// exactly what those ceilings admitted.
165    pub(crate) fn from_admitted(
166        graph: &CodeGraph,
167        selection: GraphEdgeSelection,
168        admitted: (u32, u32),
169    ) -> Result<Self, GraphAnalysisError> {
170        let indexes = SelectedIndexes::build(graph, selection, admitted);
171        let partition = partition::derive(graph, &indexes)?;
172        Ok(Self {
173            indexes: Arc::new(indexes),
174            partition: Arc::new(partition),
175        })
176    }
177
178    /// The adjacency and containment tables this selection produced.
179    pub(crate) fn indexes(&self) -> &SelectedIndexes {
180        &self.indexes
181    }
182
183    /// The declared module partition this selection settled.
184    pub(crate) fn partition(&self) -> &DeclaredModulePartition {
185        &self.partition
186    }
187
188    /// One borrowed reading of this state, beside the graph and the ceilings a
189    /// query is answered under.
190    pub(crate) fn context<'analysis>(
191        &'analysis self,
192        graph: &'analysis CodeGraph,
193        limits: GraphAnalysisLimits,
194    ) -> AnalysisContext<'analysis> {
195        AnalysisContext {
196            graph,
197            limits,
198            indexes: &self.indexes,
199            partition: &self.partition,
200        }
201    }
202}
203
204/// Everything one derived algorithm reads, gathered once.
205///
206/// The one concrete value every algorithm consumes. A borrowed
207/// [`GraphAnalysis`] and an owned cached handle each produce one, so neither
208/// owns an algorithm, a selection predicate, or a second set of indexes.
209pub(crate) struct AnalysisContext<'analysis> {
210    graph: &'analysis CodeGraph,
211    limits: GraphAnalysisLimits,
212    indexes: &'analysis SelectedIndexes,
213    partition: &'analysis DeclaredModulePartition,
214}
215
216impl<'analysis> AnalysisContext<'analysis> {
217    /// The graph every identity an answer states indexes back into.
218    pub(crate) fn graph(&self) -> &'analysis CodeGraph {
219        self.graph
220    }
221
222    /// The ceilings this answer is admitted under.
223    pub(crate) fn limits(&self) -> GraphAnalysisLimits {
224        self.limits
225    }
226
227    /// The shared indexes every query reads.
228    pub(crate) fn indexes(&self) -> &'analysis SelectedIndexes {
229        self.indexes
230    }
231
232    /// The declared module partition, derived once with the indexes.
233    pub(crate) fn declared_partition(&self) -> &'analysis DeclaredModulePartition {
234        self.partition
235    }
236}
237
238/// One borrowed graph, the selection applied to it, and the indexes that
239/// selection produced.
240pub struct GraphAnalysis<'graph> {
241    graph: &'graph CodeGraph,
242    selection: GraphEdgeSelection,
243    limits: GraphAnalysisLimits,
244    shared: SharedAnalysis,
245}
246
247impl<'graph> GraphAnalysis<'graph> {
248    /// Index one graph under one stated selection and one stated set of
249    /// ceilings.
250    ///
251    /// The node count is proved before any node-indexed table exists and the
252    /// selected-edge count is proved before any adjacency storage does, so a
253    /// refusal costs the counts alone.
254    pub fn new(
255        graph: &'graph CodeGraph,
256        selection: GraphEdgeSelection,
257        limits: GraphAnalysisLimits,
258    ) -> Result<GraphAnalysis<'graph>, GraphAnalysisError> {
259        Ok(Self {
260            graph,
261            selection,
262            limits,
263            shared: SharedAnalysis::indexed(graph, selection, limits)?,
264        })
265    }
266
267    /// The graph this analysis reads. Every identity it answers with indexes
268    /// back into this value.
269    pub fn graph(&self) -> &'graph CodeGraph {
270        self.graph
271    }
272
273    /// The selection every index was built from.
274    pub fn selection(&self) -> GraphEdgeSelection {
275        self.selection
276    }
277
278    /// The ceilings this analysis was admitted under.
279    pub fn limits(&self) -> GraphAnalysisLimits {
280        self.limits
281    }
282
283    /// The declared module partition, derived once from logical containment.
284    pub fn declared_partition(&self) -> &DeclaredModulePartition {
285        self.shared.partition()
286    }
287
288    /// Every node connected to `node` within `depth` selected steps in
289    /// `direction`, at its minimum distance, in distance then node order.
290    pub fn neighbors(
291        &self,
292        node: GraphNodeId,
293        depth: u32,
294        direction: GraphDirection,
295    ) -> Result<Box<[GraphNeighbor]>, GraphAnalysisError> {
296        traversal::neighbors(&self.context(), node, depth, direction)
297    }
298
299    /// The shortest route from `source` to `target` over selected outgoing
300    /// edges, or `None` when no route exists.
301    pub fn path(
302        &self,
303        source: GraphNodeId,
304        target: GraphNodeId,
305    ) -> Result<Option<GraphPath>, GraphAnalysisError> {
306        traversal::path(&self.context(), (source, target))
307    }
308
309    /// The selection into this graph induced by the nodes `seed` reaches
310    /// within `depth` selected steps.
311    pub fn subgraph(
312        &self,
313        seed: GraphNodeId,
314        depth: u32,
315        direction: GraphDirection,
316    ) -> Result<GraphSubgraph, GraphAnalysisError> {
317        traversal::subgraph(&self.context(), seed, depth, direction)
318    }
319
320    /// How many selected edges arrive at and leave each node, in raw node-id
321    /// order.
322    pub fn degree_centrality(&self) -> Box<[DegreeCentrality]> {
323        degree::degree(&self.context())
324    }
325
326    /// How much of the selected topology's shortest routing passes through each
327    /// node, in raw node-id order.
328    pub fn betweenness_centrality(
329        &self,
330    ) -> Result<Box<[BetweennessCentrality]>, GraphAnalysisError> {
331        betweenness::betweenness(&self.context())
332    }
333
334    /// Which nodes reach each other over selected edges.
335    pub fn strongly_connected_components(&self) -> GraphComponents {
336        components::discover(&self.context())
337    }
338
339    /// The same selected topology with every component contracted, ordered, and
340    /// still carrying every raw edge it coalesced.
341    pub fn condensation(&self) -> CondensationGraph {
342        components::condense(&self.context())
343    }
344
345    /// What the selected topology says about the module partition this graph
346    /// declares.
347    pub fn divergence(&self) -> GraphDivergence {
348        divergence::divergence(&self.context())
349    }
350
351    /// Every admitted node and selected edge, described for a consumer that
352    /// ranks or draws them.
353    pub fn layout_assist(&self) -> Result<LayoutAssistMetadata, GraphAnalysisError> {
354        layout::layout_assist(&self.context())
355    }
356
357    /// What every algorithm this view delegates to reads.
358    fn context(&self) -> AnalysisContext<'_> {
359        self.shared.context(self.graph, self.limits)
360    }
361}
362
363/// The node and selected-edge counts one graph states, both proved against the
364/// stated ceilings.
365///
366/// The one admission owner. A borrowed analysis proves the pair before it
367/// indexes anything, and a cached lookup proves the same pair before it can
368/// answer with indexes admitted under an earlier, looser ceiling.
369pub(crate) fn admitted_counts(
370    graph: &CodeGraph,
371    selection: GraphEdgeSelection,
372    limits: GraphAnalysisLimits,
373) -> Result<(u32, u32), GraphAnalysisError> {
374    let nodes = admitted_nodes(graph, limits)?;
375    let edges = admitted_edges(graph, selection, limits)?;
376    Ok((nodes, edges))
377}
378
379/// Every edge one selection admits, in raw edge-id order.
380///
381/// The one place the raw edge slice is read and the one place the selection is
382/// applied, so the counted set and the indexed set cannot differ.
383fn selected(graph: &CodeGraph, selection: GraphEdgeSelection) -> impl Iterator<Item = &GraphEdge> {
384    graph
385        .edges()
386        .iter()
387        .filter(move |edge| selection.allows(edge))
388}
389
390/// The graph's node count, proved against the stated ceiling.
391fn admitted_nodes(
392    graph: &CodeGraph,
393    limits: GraphAnalysisLimits,
394) -> Result<u32, GraphAnalysisError> {
395    let count = position(graph.nodes().len());
396    match count <= limits.max_nodes() {
397        true => Ok(count),
398        false => Err(GraphAnalysisError::NodeLimitExceeded {
399            count,
400            limit: limits.max_nodes(),
401        }),
402    }
403}
404
405/// The selected edge count, proved against the stated ceiling.
406fn admitted_edges(
407    graph: &CodeGraph,
408    selection: GraphEdgeSelection,
409    limits: GraphAnalysisLimits,
410) -> Result<u32, GraphAnalysisError> {
411    let count = position(selected(graph, selection).count());
412    match count <= limits.max_selected_edges() {
413        true => Ok(count),
414        false => Err(GraphAnalysisError::SelectedEdgeLimitExceeded {
415            count,
416            limit: limits.max_selected_edges(),
417        }),
418    }
419}
420
421/// One adjacency entry: the edge, and the endpoint it reaches.
422fn entry(edge: GraphEdgeId, node: GraphNodeId) -> SelectedAdjacency {
423    SelectedAdjacency { edge, node }
424}
425
426/// Record one selected edge in the list its endpoint owns.
427fn append(lists: &mut [Vec<SelectedAdjacency>], node: GraphNodeId, entry: SelectedAdjacency) {
428    if let Some(list) = lists.get_mut(index_of(node.index())) {
429        list.push(entry);
430    }
431}
432
433/// Record the containment parent one child is owned by.
434fn claim(parents: &mut [Option<GraphNodeId>], child: GraphNodeId, parent: GraphNodeId) {
435    if let Some(slot) = parents.get_mut(index_of(child.index())) {
436        *slot = Some(parent);
437    }
438}
439
440/// One filled adjacency table, sealed against further growth.
441fn sealed(lists: Vec<Vec<SelectedAdjacency>>) -> Box<[Box<[SelectedAdjacency]>]> {
442    lists
443        .into_iter()
444        .map(Vec::into_boxed_slice)
445        .collect::<Vec<Box<[SelectedAdjacency]>>>()
446        .into_boxed_slice()
447}
448
449/// One node's adjacency list, or nothing at all for a node this graph does not
450/// hold.
451fn borrowed(lists: &[Box<[SelectedAdjacency]>], node: GraphNodeId) -> &[SelectedAdjacency] {
452    match lists.get(index_of(node.index())) {
453        Some(list) => list,
454        None => &[],
455    }
456}