Skip to main content

pedant_graph/
graph.rs

1//! The immutable graph value and the one checked store every build inserts
2//! through.
3
4use pedant_types::ResolutionTier;
5use serde::Serialize;
6
7use crate::containment::ContainmentEdge;
8use crate::edge::{EdgeDraft, GraphEdge, GraphEdgeOrigin};
9use crate::error::GraphBuildError;
10use crate::id::{GraphEdgeId, GraphNodeId, GraphReferenceId, index_of, position};
11use crate::limits::{GraphCollection, GraphLimits};
12use crate::node::{GraphNode, NodeDraft};
13use crate::reference::{GraphReference, ReferenceDraft};
14
15/// One deterministic, language-tagged code topology.
16///
17/// Immutable after construction: fields are private, every accessor borrows,
18/// and the value implements neither `Clone` nor `Deserialize`. A consumer that
19/// wants a graph builds one from the facts it holds rather than hydrating one
20/// whose records nothing validated.
21#[derive(Serialize, Debug, PartialEq, Eq)]
22pub struct CodeGraph {
23    schema_version: u32,
24    tier: ResolutionTier,
25    nodes: Box<[GraphNode]>,
26    containment: Box<[ContainmentEdge]>,
27    references: Box<[GraphReference]>,
28    edges: Box<[GraphEdge]>,
29}
30
31impl CodeGraph {
32    /// The version of the serialized topology contract this crate emits.
33    pub const SCHEMA_VERSION: u32 = 1;
34
35    /// The version of the serialized topology contract this value carries.
36    pub fn schema_version(&self) -> u32 {
37        self.schema_version
38    }
39
40    /// Which resolution mechanism produced the facts behind this graph.
41    pub fn tier(&self) -> ResolutionTier {
42        self.tier
43    }
44
45    /// Every node, indexed by its own dense identity.
46    pub fn nodes(&self) -> &[GraphNode] {
47        &self.nodes
48    }
49
50    /// The node one identity selects.
51    pub fn node(&self, id: GraphNodeId) -> Option<&GraphNode> {
52        self.nodes.get(index_of(id.index()))
53    }
54
55    /// Every containment edge, sorted by child.
56    pub fn containment(&self) -> &[ContainmentEdge] {
57        &self.containment
58    }
59
60    /// Every reference record, indexed by its own dense identity.
61    pub fn references(&self) -> &[GraphReference] {
62        &self.references
63    }
64
65    /// The reference record one identity selects.
66    pub fn reference(&self, id: GraphReferenceId) -> Option<&GraphReference> {
67        self.references.get(index_of(id.index()))
68    }
69
70    /// Every edge, indexed by its own dense identity.
71    pub fn edges(&self) -> &[GraphEdge] {
72        &self.edges
73    }
74
75    /// The edge one identity selects.
76    pub fn edge(&self, id: GraphEdgeId) -> Option<&GraphEdge> {
77        self.edges.get(index_of(id.index()))
78    }
79
80    /// Whether this graph is still admissible under one set of ceilings.
81    ///
82    /// A graph is reused rather than rebuilt only when the caller's current
83    /// ceilings would have admitted it. The collections are compared in the
84    /// order a build fills them — nodes, then reference records, then edges —
85    /// so the refusal a lowered ceiling produces names the collection direct
86    /// construction would have reached first.
87    pub(crate) fn admissible(&self, limits: GraphLimits) -> Result<(), GraphBuildError> {
88        [
89            (GraphCollection::Node, self.nodes.len()),
90            (GraphCollection::Reference, self.references.len()),
91            (GraphCollection::Edge, self.edges.len()),
92        ]
93        .into_iter()
94        .find(|(collection, held)| position(*held) > limits.ceiling(*collection))
95        .map_or(Ok(()), |(collection, _)| {
96            Err(GraphBuildError::CapacityExceeded {
97                collection,
98                limit: limits.ceiling(collection),
99            })
100        })
101    }
102}
103
104/// How many records one build states before its first pass.
105///
106/// Every count is a stated size, never a ceiling: the store reserves the
107/// smaller of the count and the limit that collection refuses at.
108pub(crate) struct RecordCapacity {
109    /// The unit containers and definition nodes the report states.
110    pub(crate) nodes: usize,
111    /// The reference records the report states.
112    pub(crate) references: usize,
113    /// The dependency and candidate edges the inputs state.
114    pub(crate) edges: usize,
115}
116
117/// The growing record store one build inserts through.
118///
119/// Every identity is minted by [`GraphRecords::admit`], so the configured
120/// ceiling and the fixed-width identity ceiling are one check and a refusal
121/// never leaves a partially inserted record behind. [`GraphRecords::contain`]
122/// mints nothing: it states a relation between identities this store has
123/// already issued.
124pub(crate) struct GraphRecords {
125    limits: GraphLimits,
126    nodes: Vec<GraphNode>,
127    containment: Vec<ContainmentEdge>,
128    references: Vec<(GraphReferenceId, ReferenceDraft)>,
129    reference_edges: Vec<Vec<GraphEdgeId>>,
130    edges: Vec<GraphEdge>,
131}
132
133impl GraphRecords {
134    /// An empty store bounded by `limits`, sized for `capacity`.
135    pub(crate) fn new(limits: GraphLimits, capacity: RecordCapacity) -> Self {
136        let nodes = reserved(capacity.nodes, limits.ceiling(GraphCollection::Node));
137        let references = reserved(
138            capacity.references,
139            limits.ceiling(GraphCollection::Reference),
140        );
141        Self {
142            limits,
143            nodes: Vec::with_capacity(nodes),
144            containment: Vec::with_capacity(nodes),
145            references: Vec::with_capacity(references),
146            reference_edges: Vec::with_capacity(references),
147            edges: Vec::with_capacity(reserved(
148                capacity.edges,
149                limits.ceiling(GraphCollection::Edge),
150            )),
151        }
152    }
153
154    /// How much a table sized from a stated count of one collection reserves.
155    ///
156    /// The ceilings a build is bounded by are the store's, so a table an
157    /// assembly sizes for its own pass asks the store rather than carrying a
158    /// second copy of the limits it would have to keep in step.
159    pub(crate) fn reserved(&self, collection: GraphCollection, stated: usize) -> usize {
160        reserved(stated, self.limits.ceiling(collection))
161    }
162
163    /// Append one node, or refuse before the store is touched.
164    pub(crate) fn insert_node(&mut self, draft: NodeDraft) -> Result<GraphNodeId, GraphBuildError> {
165        let index = self.admit(GraphCollection::Node)?;
166        let id = GraphNodeId::new(index);
167        self.nodes.push(GraphNode::new(id, draft));
168        Ok(id)
169    }
170
171    /// Append one reference record, or refuse before the store is touched.
172    pub(crate) fn insert_reference(
173        &mut self,
174        draft: ReferenceDraft,
175    ) -> Result<GraphReferenceId, GraphBuildError> {
176        let index = self.admit(GraphCollection::Reference)?;
177        let id = GraphReferenceId::new(index);
178        self.references.push((id, draft));
179        self.reference_edges.push(Vec::new());
180        Ok(id)
181    }
182
183    /// Append one edge and link it to the record that produced it, or refuse
184    /// before the store is touched.
185    ///
186    /// An edge whose origin names a reference is attached to that record here,
187    /// so minting and linking are one step and no identity can reach
188    /// [`GraphReference::edges`] without an edge behind it. A reference that
189    /// answers for no record refuses before the edge exists: dropping the link
190    /// would leave the edge in the graph and its record claiming it produced
191    /// nothing.
192    pub(crate) fn insert_edge(&mut self, draft: EdgeDraft) -> Result<GraphEdgeId, GraphBuildError> {
193        let index = self.admit(GraphCollection::Edge)?;
194        let id = GraphEdgeId::new(index);
195        self.link_origin(&draft.origin, id)?;
196        self.edges.push(GraphEdge::new(id, draft));
197        Ok(id)
198    }
199
200    /// Record that the reference an edge names produced it.
201    ///
202    /// A dependency edge links nothing: the declaration it carries is its own
203    /// evidence, and no record claims it.
204    fn link_origin(
205        &mut self,
206        origin: &GraphEdgeOrigin,
207        edge: GraphEdgeId,
208    ) -> Result<(), GraphBuildError> {
209        let reference = match origin {
210            GraphEdgeOrigin::Reference { reference } => *reference,
211            GraphEdgeOrigin::Dependency { .. } => return Ok(()),
212        };
213        match self.reference_edges.get_mut(index_of(reference.index())) {
214            Some(produced) => {
215                produced.push(edge);
216                Ok(())
217            }
218            None => Err(GraphBuildError::MissingReferenceRecord {
219                reference: reference.index(),
220            }),
221        }
222    }
223
224    /// Record that `parent` logically owns `child`.
225    pub(crate) fn contain(&mut self, parent: GraphNodeId, child: GraphNodeId) {
226        self.containment.push(ContainmentEdge::new(parent, child));
227    }
228
229    /// Every containment edge stated so far.
230    pub(crate) fn containment(&self) -> &[ContainmentEdge] {
231        &self.containment
232    }
233
234    /// How many nodes the store holds.
235    pub(crate) fn node_count(&self) -> usize {
236        self.nodes.len()
237    }
238
239    /// Seal the store into one immutable graph.
240    ///
241    /// Containment sorts by child then parent, which is a total order over the
242    /// stated edges rather than one that holds only while a separate validator
243    /// rejects a twice-claimed child. Each reference record keeps the identity
244    /// its insertion minted, so a record can never be renumbered away from the
245    /// edges that name it.
246    pub(crate) fn finish(mut self, tier: ResolutionTier) -> CodeGraph {
247        self.containment
248            .sort_unstable_by_key(|edge| (edge.child(), edge.parent()));
249        let references = self
250            .references
251            .into_iter()
252            .zip(self.reference_edges)
253            .map(|((id, draft), edges)| GraphReference::new(id, draft, edges.into_boxed_slice()))
254            .collect();
255        CodeGraph {
256            schema_version: CodeGraph::SCHEMA_VERSION,
257            tier,
258            nodes: self.nodes.into_boxed_slice(),
259            containment: self.containment.into_boxed_slice(),
260            references,
261            edges: self.edges.into_boxed_slice(),
262        }
263    }
264
265    /// The identity the next entry of `collection` would take.
266    ///
267    /// The one place a graph identity is minted: it proves the configured
268    /// ceiling and the fixed-width ceiling together, before any mutation. The
269    /// count is read from the named collection rather than supplied, so an
270    /// identity can never be minted from the length of another slice.
271    fn admit(&self, collection: GraphCollection) -> Result<u32, GraphBuildError> {
272        let count = match collection {
273            GraphCollection::Node => self.nodes.len(),
274            GraphCollection::Reference => self.references.len(),
275            GraphCollection::Edge => self.edges.len(),
276        };
277        let limit = self.limits.ceiling(collection);
278        u32::try_from(count)
279            .ok()
280            .filter(|next| *next < limit)
281            .ok_or(GraphBuildError::CapacityExceeded { collection, limit })
282    }
283}
284
285/// How much one collection reserves for a stated count.
286///
287/// Never more than the ceiling that collection refuses at, so a build the
288/// limits are going to refuse allocates no more than the graph it would have
289/// admitted. Every table sized from a stated count reserves through here, so
290/// one rule bounds them all.
291pub(crate) fn reserved(stated: usize, ceiling: u32) -> usize {
292    stated.min(index_of(ceiling))
293}