Skip to main content

pedant_graph/rust/
cache.rs

1//! The caller-created cache of already built Rust graphs.
2//!
3//! It accepts exactly what the direct builders accept and loads nothing of its
4//! own: no project, no source, no path, no parser, no semantic context, no
5//! resolver, no process, and no filesystem event. The caller stays responsible
6//! for supplying a current snapshot beside the resolution validated against it.
7//!
8//! Reuse is decided by the complete claim rather than by the snapshot
9//! fingerprint alone. The fingerprint identifies repository content and the
10//! selected units; a graph also depends on the validated unit bindings and the
11//! complete report supplied beside that snapshot, and two valid reports over
12//! one snapshot describe two different graphs.
13
14use std::sync::Arc;
15
16use pedant_core::resolution::rust::{
17    RustResolutionSnapshot, RustSnapshotFingerprint, RustTargetResolution, RustUnitBinding,
18    TargetId,
19};
20use pedant_types::ResolutionReport;
21
22use crate::cache::{
23    BoundedStore, CacheCategory, CacheCounters, CacheEvent, CachedCodeGraph, CachedGraphState,
24    GraphCacheLimits, GraphCacheStats,
25};
26use crate::error::GraphBuildError;
27use crate::limits::GraphLimits;
28
29use crate::projection::assembly;
30
31use super::projection;
32use super::reuse::ProjectionStore;
33
34/// The complete claim one retained graph was built from.
35///
36/// The fingerprint narrows the comparison and exact typed equality decides it,
37/// so a custom valid report over an unchanged snapshot can never be answered
38/// with a graph built from another report.
39struct ExactGraphClaim {
40    fingerprint: RustSnapshotFingerprint,
41    root_target: TargetId,
42    bindings: Box<[RustUnitBinding]>,
43    report: Arc<ResolutionReport>,
44}
45
46impl ExactGraphClaim {
47    /// The claim one validated pairing states.
48    fn new(snapshot: &RustResolutionSnapshot, resolution: &RustTargetResolution) -> Self {
49        Self {
50            fingerprint: snapshot.fingerprint(),
51            root_target: resolution.root_target(),
52            bindings: resolution.units().into(),
53            report: resolution.shared_report(),
54        }
55    }
56
57    /// Whether two claims describe the same report.
58    ///
59    /// One handle on a report proves equality without reading it. Two handles
60    /// on equal reports are equal too, so a caller that rebuilt its report from
61    /// the same facts still reuses the graph those facts describe.
62    fn states_one_report(&self, other: &Self) -> bool {
63        Arc::ptr_eq(&self.report, &other.report) || self.report == other.report
64    }
65}
66
67/// Cheapest fields first: a differing fingerprint decides the comparison before
68/// a binding slice or a whole report is read.
69impl PartialEq for ExactGraphClaim {
70    fn eq(&self, other: &Self) -> bool {
71        self.fingerprint == other.fingerprint
72            && self.root_target == other.root_target
73            && self.bindings == other.bindings
74            && self.states_one_report(other)
75    }
76}
77
78/// A caller-owned cache of graphs built from complete resolution claims.
79///
80/// The cache is mutated through `&mut self`, so a host chooses its own
81/// synchronization policy for construction. Every handle it returns may outlive
82/// it and may be shared across threads.
83pub struct GraphCache {
84    counters: Arc<CacheCounters>,
85    limits: GraphCacheLimits,
86    graphs: BoundedStore<ExactGraphClaim, Arc<CachedGraphState>>,
87    projections: ProjectionStore,
88}
89
90impl GraphCache {
91    /// An empty cache bounded by one host-chosen retention budget.
92    pub fn new(limits: GraphCacheLimits) -> Self {
93        let counters = Arc::new(CacheCounters::default());
94        Self {
95            graphs: BoundedStore::new(limits.max_exact_graphs()),
96            projections: ProjectionStore::new(
97                limits.max_source_projections(),
98                Arc::clone(&counters),
99            ),
100            limits,
101            counters,
102        }
103    }
104
105    /// Project one snapshot-bound Rust resolution, reusing an exact earlier
106    /// answer when the caller states exactly the same claim.
107    ///
108    /// The returned graph is the one [`crate::build_rust_graph_with_limits`]
109    /// returns for the same arguments, whatever this cache retained first.
110    ///
111    /// # Errors
112    ///
113    /// The same refusals as the direct builders. Both identity refusals are
114    /// taken before any retained state is examined, so a mismatched pairing
115    /// cannot be answered from, or recorded against, this cache.
116    pub fn build_rust_graph(
117        &mut self,
118        snapshot: &RustResolutionSnapshot,
119        resolution: &RustTargetResolution,
120        limits: GraphLimits,
121    ) -> Result<CachedCodeGraph, GraphBuildError> {
122        projection::validate(snapshot, resolution)?;
123        let claim = ExactGraphClaim::new(snapshot, resolution);
124        match self.graphs.get(&claim) {
125            Some(retained) => self.reuse(retained, limits),
126            None => self.build_missing(claim, (snapshot, resolution), limits),
127        }
128    }
129
130    /// What this cache has done so far.
131    pub fn stats(&self) -> GraphCacheStats {
132        self.counters.snapshot()
133    }
134
135    /// Drop every retained graph and every retained source projection.
136    ///
137    /// Cumulative statistics are not reset, and no handle a caller holds is
138    /// invalidated: the cache drops its own strong references and nothing else.
139    pub fn clear(&mut self) {
140        self.graphs.clear();
141        self.projections.clear();
142    }
143
144    /// Return one retained graph, under the caller's current ceilings.
145    ///
146    /// The ceilings are not part of a graph's identity, because admitted output
147    /// is independent of the ceiling that admitted it. They are reapplied here
148    /// in construction order, so a caller that lowered one is refused exactly as
149    /// a direct build would refuse it.
150    fn reuse(
151        &self,
152        retained: Arc<CachedGraphState>,
153        limits: GraphLimits,
154    ) -> Result<CachedCodeGraph, GraphBuildError> {
155        self.counters
156            .record(CacheCategory::ExactGraph, CacheEvent::Hit, 1);
157        retained.graph().admissible(limits)?;
158        Ok(CachedCodeGraph::new(retained))
159    }
160
161    /// Build one graph this cache holds no answer for, and retain it.
162    ///
163    /// The planner examines every source this pairing states exactly once,
164    /// against the projections this cache retained. A source whose claim still
165    /// holds is answered from that store and derived again by nothing; every
166    /// other source is derived and retained. Removed and superseded projections
167    /// never reach the assembly, because the walk is over the sources the
168    /// current plan states, in the order it states them, and a selected entry
169    /// answers only when the current report still states its claim.
170    fn build_missing(
171        &mut self,
172        claim: ExactGraphClaim,
173        stated: (&RustResolutionSnapshot, &RustTargetResolution),
174        limits: GraphLimits,
175    ) -> Result<CachedCodeGraph, GraphBuildError> {
176        let (snapshot, resolution) = stated;
177        self.counters
178            .record(CacheCategory::ExactGraph, CacheEvent::Miss, 1);
179        let planned = projection::plan(snapshot, resolution, &mut self.projections)?;
180        let state = Arc::new(CachedGraphState::new(
181            assembly::assemble(&planned, limits)?,
182            self.limits,
183            Arc::clone(&self.counters),
184        ));
185        let evicted = self.graphs.retain(claim, Arc::clone(&state));
186        self.counters
187            .record(CacheCategory::ExactGraph, CacheEvent::Eviction, evicted);
188        Ok(CachedCodeGraph::new(state))
189    }
190}