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}