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    /// Append one node, or refuse before the store is touched.
155    pub(crate) fn insert_node(&mut self, draft: NodeDraft) -> Result<GraphNodeId, GraphBuildError> {
156        let index = self.admit(GraphCollection::Node)?;
157        let id = GraphNodeId::new(index);
158        self.nodes.push(GraphNode::new(id, draft));
159        Ok(id)
160    }
161
162    /// Append one reference record, or refuse before the store is touched.
163    pub(crate) fn insert_reference(
164        &mut self,
165        draft: ReferenceDraft,
166    ) -> Result<GraphReferenceId, GraphBuildError> {
167        let index = self.admit(GraphCollection::Reference)?;
168        let id = GraphReferenceId::new(index);
169        self.references.push((id, draft));
170        self.reference_edges.push(Vec::new());
171        Ok(id)
172    }
173
174    /// Append one edge and link it to the record that produced it, or refuse
175    /// before the store is touched.
176    ///
177    /// An edge whose origin names a reference is attached to that record here,
178    /// so minting and linking are one step and no identity can reach
179    /// [`GraphReference::edges`] without an edge behind it. A reference that
180    /// answers for no record refuses before the edge exists: dropping the link
181    /// would leave the edge in the graph and its record claiming it produced
182    /// nothing.
183    pub(crate) fn insert_edge(&mut self, draft: EdgeDraft) -> Result<GraphEdgeId, GraphBuildError> {
184        let index = self.admit(GraphCollection::Edge)?;
185        let id = GraphEdgeId::new(index);
186        self.link_origin(&draft.origin, id)?;
187        self.edges.push(GraphEdge::new(id, draft));
188        Ok(id)
189    }
190
191    /// Record that the reference an edge names produced it.
192    ///
193    /// A dependency edge links nothing: the declaration it carries is its own
194    /// evidence, and no record claims it.
195    fn link_origin(
196        &mut self,
197        origin: &GraphEdgeOrigin,
198        edge: GraphEdgeId,
199    ) -> Result<(), GraphBuildError> {
200        let reference = match origin {
201            GraphEdgeOrigin::Reference { reference } => *reference,
202            GraphEdgeOrigin::Dependency { .. } => return Ok(()),
203        };
204        match self.reference_edges.get_mut(index_of(reference.index())) {
205            Some(produced) => {
206                produced.push(edge);
207                Ok(())
208            }
209            None => Err(GraphBuildError::MissingReferenceRecord {
210                reference: reference.index(),
211            }),
212        }
213    }
214
215    /// Record that `parent` logically owns `child`.
216    pub(crate) fn contain(&mut self, parent: GraphNodeId, child: GraphNodeId) {
217        self.containment.push(ContainmentEdge::new(parent, child));
218    }
219
220    /// Every containment edge stated so far.
221    pub(crate) fn containment(&self) -> &[ContainmentEdge] {
222        &self.containment
223    }
224
225    /// How many nodes the store holds.
226    pub(crate) fn node_count(&self) -> usize {
227        self.nodes.len()
228    }
229
230    /// Seal the store into one immutable graph.
231    ///
232    /// Containment sorts by child then parent, which is a total order over the
233    /// stated edges rather than one that holds only while a separate validator
234    /// rejects a twice-claimed child. Each reference record keeps the identity
235    /// its insertion minted, so a record can never be renumbered away from the
236    /// edges that name it.
237    pub(crate) fn finish(mut self, tier: ResolutionTier) -> CodeGraph {
238        self.containment
239            .sort_unstable_by_key(|edge| (edge.child(), edge.parent()));
240        let references = self
241            .references
242            .into_iter()
243            .zip(self.reference_edges)
244            .map(|((id, draft), edges)| GraphReference::new(id, draft, edges.into_boxed_slice()))
245            .collect();
246        CodeGraph {
247            schema_version: CodeGraph::SCHEMA_VERSION,
248            tier,
249            nodes: self.nodes.into_boxed_slice(),
250            containment: self.containment.into_boxed_slice(),
251            references,
252            edges: self.edges.into_boxed_slice(),
253        }
254    }
255
256    /// The identity the next entry of `collection` would take.
257    ///
258    /// The one place a graph identity is minted: it proves the configured
259    /// ceiling and the fixed-width ceiling together, before any mutation. The
260    /// count is read from the named collection rather than supplied, so an
261    /// identity can never be minted from the length of another slice.
262    fn admit(&self, collection: GraphCollection) -> Result<u32, GraphBuildError> {
263        let count = match collection {
264            GraphCollection::Node => self.nodes.len(),
265            GraphCollection::Reference => self.references.len(),
266            GraphCollection::Edge => self.edges.len(),
267        };
268        let limit = self.limits.ceiling(collection);
269        u32::try_from(count)
270            .ok()
271            .filter(|next| *next < limit)
272            .ok_or(GraphBuildError::CapacityExceeded { collection, limit })
273    }
274}
275
276/// How much one collection reserves for a stated count.
277///
278/// Never more than the ceiling that collection refuses at, so a build the
279/// limits are going to refuse allocates no more than the graph it would have
280/// admitted. Every table sized from a stated count reserves through here, so
281/// one rule bounds them all.
282pub(crate) fn reserved(stated: usize, ceiling: u32) -> usize {
283    stated.min(index_of(ceiling))
284}