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}