Skip to main content

pedant_graph/analysis/
partition.rs

1//! The declared module partition, derived from logical containment alone.
2//!
3//! Every container is a partition root, every node belongs to the nearest
4//! container above it, and a container belongs to itself. Where a node's bytes
5//! sit never enters the answer: a file node follows containment into the unit
6//! that instantiates it, exactly as a definition follows containment into the
7//! module that declares it.
8
9use crate::graph::CodeGraph;
10use crate::id::{GraphNodeId, index_of, position};
11use crate::node::GraphNodeKind;
12
13use super::error::GraphAnalysisError;
14use super::index::SelectedIndexes;
15
16/// Which container owns each node, and which nodes each container owns.
17///
18/// Derived once per analysis. Both views come from one walk, so an owner and a
19/// membership list can never disagree.
20pub struct DeclaredModulePartition {
21    owners: Box<[GraphNodeId]>,
22    roots: Box<[GraphNodeId]>,
23    members: Box<[Box<[GraphNodeId]>]>,
24}
25
26impl DeclaredModulePartition {
27    /// The nearest container above one node, which is the node itself when it
28    /// is a container.
29    pub fn owner(&self, node: GraphNodeId) -> Option<GraphNodeId> {
30        self.owners.get(index_of(node.index())).copied()
31    }
32
33    /// Every partition root, in node-id order.
34    pub fn roots(&self) -> &[GraphNodeId] {
35        &self.roots
36    }
37
38    /// Which container owns each node, in node-id order.
39    ///
40    /// The dense table the derivation settled, for a consumer that walks every
41    /// node rather than asking about one. A public caller asks through
42    /// [`DeclaredModulePartition::owner`], which answers for an identity this
43    /// graph may not hold.
44    pub(crate) fn owners(&self) -> &[GraphNodeId] {
45        &self.owners
46    }
47
48    /// Every node one root owns, in node-id order, including the root itself.
49    pub fn members(&self, root: GraphNodeId) -> Option<&[GraphNodeId]> {
50        self.roots
51            .binary_search(&root)
52            .ok()
53            .and_then(|at| self.members.get(at))
54            .map(|held| &**held)
55    }
56}
57
58/// The partial answer one derivation carries while it walks.
59struct Ownership {
60    containers: Box<[bool]>,
61    owners: Box<[Option<GraphNodeId>]>,
62    chain: Vec<GraphNodeId>,
63}
64
65impl Ownership {
66    /// One unsettled ownership table for `graph`.
67    fn new(graph: &CodeGraph) -> Self {
68        let containers: Box<[bool]> = graph
69            .nodes()
70            .iter()
71            .map(|node| matches!(node.kind(), GraphNodeKind::Container { .. }))
72            .collect();
73        let owners = vec![None; containers.len()].into_boxed_slice();
74        Self {
75            containers,
76            owners,
77            chain: Vec::new(),
78        }
79    }
80
81    /// The owner one node already settled on, if a previous walk reached it.
82    fn settled(&self, node: GraphNodeId) -> Option<GraphNodeId> {
83        self.owners.get(index_of(node.index())).copied().flatten()
84    }
85
86    /// Whether one node is a container, and therefore its own owner.
87    fn is_container(&self, node: GraphNodeId) -> bool {
88        self.containers
89            .get(index_of(node.index()))
90            .copied()
91            .unwrap_or(false)
92    }
93
94    /// Record one node on the chain currently being walked.
95    fn push(&mut self, node: GraphNodeId) {
96        self.chain.push(node);
97    }
98
99    /// Settle every node of the current chain on one owner.
100    fn settle(&mut self, owner: GraphNodeId) {
101        for node in self.chain.drain(..) {
102            if let Some(slot) = self.owners.get_mut(index_of(node.index())) {
103                *slot = Some(owner);
104            }
105        }
106    }
107
108    /// Abandon the current chain, leaving its nodes unsettled.
109    fn abandon(&mut self) {
110        self.chain.clear();
111    }
112
113    /// How many nodes this table addresses.
114    fn width(&self) -> usize {
115        self.owners.len()
116    }
117
118    /// The settled partition, or the first node no container owns.
119    fn finish(self) -> Result<DeclaredModulePartition, GraphAnalysisError> {
120        let unsettled = self.owners.iter().position(Option::is_none);
121        match unsettled {
122            Some(at) => Err(GraphAnalysisError::ConsumedGraphInvariant {
123                node: GraphNodeId::new(position(at)),
124            }),
125            None => Ok(assembled(
126                self.owners.iter().flatten().copied().collect(),
127                &self.containers,
128            )),
129        }
130    }
131}
132
133/// Derive the declared module partition of one graph.
134///
135/// Graph construction proves every node is reachable from a container root, so
136/// the refusal below is the exhaustiveness branch of that guarantee rather than
137/// a refusal an ordinary repository can reach.
138pub(crate) fn derive(
139    graph: &CodeGraph,
140    indexes: &SelectedIndexes,
141) -> Result<DeclaredModulePartition, GraphAnalysisError> {
142    let mut ownership = Ownership::new(graph);
143    for node in graph.nodes() {
144        let owner = nearest_container(&mut ownership, indexes, node.id());
145        match owner {
146            Some(found) => ownership.settle(found),
147            None => ownership.abandon(),
148        }
149    }
150    ownership.finish()
151}
152
153/// Walk up the containment chain from one node to the nearest container.
154///
155/// Iterative, and bounded by the node count, so neither a deep hierarchy nor a
156/// containment cycle no validator saw can run without end.
157fn nearest_container(
158    ownership: &mut Ownership,
159    indexes: &SelectedIndexes,
160    start: GraphNodeId,
161) -> Option<GraphNodeId> {
162    let mut current = Some(start);
163    for _ in 0..ownership.width() {
164        let node = current?;
165        let settled = ownership.settled(node);
166        match settled {
167            Some(owner) => return Some(owner),
168            None => ownership.push(node),
169        }
170        match ownership.is_container(node) {
171            true => return Some(node),
172            false => current = indexes.parent(node),
173        }
174    }
175    None
176}
177
178/// One settled owner table, grouped into the members each root owns.
179fn assembled(owners: Box<[GraphNodeId]>, containers: &[bool]) -> DeclaredModulePartition {
180    let roots: Box<[GraphNodeId]> = containers
181        .iter()
182        .enumerate()
183        .filter(|(_, container)| **container)
184        .map(|(at, _)| GraphNodeId::new(position(at)))
185        .collect();
186    let members = grouped(&owners, &roots);
187    DeclaredModulePartition {
188        owners,
189        roots,
190        members,
191    }
192}
193
194/// Every root's members, in node-id order, parallel to `roots`.
195fn grouped(owners: &[GraphNodeId], roots: &[GraphNodeId]) -> Box<[Box<[GraphNodeId]>]> {
196    let mut buckets: Vec<Vec<GraphNodeId>> = vec![Vec::new(); roots.len()];
197    for (at, owner) in owners.iter().enumerate() {
198        record(&mut buckets, roots, *owner, GraphNodeId::new(position(at)));
199    }
200    buckets
201        .into_iter()
202        .map(Vec::into_boxed_slice)
203        .collect::<Vec<Box<[GraphNodeId]>>>()
204        .into_boxed_slice()
205}
206
207/// Record one node among the members of the root that owns it.
208fn record(
209    buckets: &mut [Vec<GraphNodeId>],
210    roots: &[GraphNodeId],
211    owner: GraphNodeId,
212    node: GraphNodeId,
213) {
214    let bucket = roots
215        .binary_search(&owner)
216        .ok()
217        .and_then(|at| buckets.get_mut(at));
218    if let Some(held) = bucket {
219        held.push(node);
220    }
221}