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