Skip to main content

saf_core/
summary_registry.rs

1//! Unified lookup for function summaries from all sources.
2//!
3//! The [`SummaryRegistry`] provides a three-tier priority lookup for function
4//! summaries:
5//!
6//! 1. **User specs** (highest priority) -- hand-authored YAML overrides
7//! 2. **Computed** -- analysis-derived summaries
8//! 3. **Defaults** (lowest priority) -- shipped built-in specs
9//!
10//! This unifies YAML-authored specs and analysis-computed summaries into a
11//! single lookup interface, used by incremental and compositional analyses.
12
13use std::collections::BTreeMap;
14
15use crate::ids::FunctionId;
16use crate::spec::SpecRegistry;
17use crate::summary::FunctionSummary;
18
19/// Unified registry for function summaries with three-tier priority lookup.
20///
21/// When looking up a function, the registry checks each tier in order:
22/// user specs > computed > defaults. The first match wins.
23#[derive(Debug, Clone, Default)]
24pub struct SummaryRegistry {
25    /// User-edited YAML specs converted to summaries (highest priority).
26    user_specs: BTreeMap<FunctionId, FunctionSummary>,
27    /// Analysis-computed summaries.
28    computed: BTreeMap<FunctionId, FunctionSummary>,
29    /// Default shipped specs converted to summaries (lowest priority).
30    defaults: BTreeMap<FunctionId, FunctionSummary>,
31}
32
33impl SummaryRegistry {
34    /// Create an empty registry.
35    #[must_use]
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Look up a summary with priority: user > computed > default.
41    ///
42    /// Returns the highest-priority summary for the given function, or `None`
43    /// if no summary exists in any tier.
44    #[must_use]
45    pub fn get(&self, id: &FunctionId) -> Option<&FunctionSummary> {
46        self.user_specs
47            .get(id)
48            .or_else(|| self.computed.get(id))
49            .or_else(|| self.defaults.get(id))
50    }
51
52    /// Insert an analysis-computed summary into the computed tier.
53    ///
54    /// If a computed summary already exists for this function, it is replaced.
55    pub fn insert_computed(&mut self, summary: FunctionSummary) {
56        self.computed.insert(summary.function_id, summary);
57    }
58
59    /// Insert a user-spec summary into the user tier (highest priority).
60    ///
61    /// If a user summary already exists for this function, it is replaced.
62    pub fn insert_user(&mut self, summary: FunctionSummary) {
63        self.user_specs.insert(summary.function_id, summary);
64    }
65
66    /// Insert a default summary into the defaults tier (lowest priority).
67    ///
68    /// If a default summary already exists for this function, it is replaced.
69    pub fn insert_default(&mut self, summary: FunctionSummary) {
70        self.defaults.insert(summary.function_id, summary);
71    }
72
73    /// Load all exact-match specs from a [`SpecRegistry`] and convert them to
74    /// summaries in the defaults tier.
75    ///
76    /// Each spec is converted via [`FunctionSummary::from_spec`] with a
77    /// `FunctionId` derived from the spec's name. Pattern-based specs are
78    /// skipped (they require runtime name resolution at call sites).
79    pub fn load_specs(&mut self, registry: &SpecRegistry) {
80        for spec in registry.iter() {
81            let function_id = FunctionId::derive(spec.name.as_bytes());
82            let summary = FunctionSummary::from_spec(spec, function_id);
83            self.defaults.insert(function_id, summary);
84        }
85    }
86
87    /// Load all exact-match specs from a [`SpecRegistry`] into the user tier.
88    ///
89    /// Same as [`load_specs`](Self::load_specs) but inserts into the
90    /// highest-priority user tier.
91    pub fn load_user_specs(&mut self, registry: &SpecRegistry) {
92        for spec in registry.iter() {
93            let function_id = FunctionId::derive(spec.name.as_bytes());
94            let summary = FunctionSummary::from_spec(spec, function_id);
95            self.user_specs.insert(function_id, summary);
96        }
97    }
98
99    /// Get the total number of summaries across all tiers.
100    ///
101    /// Note: if the same function appears in multiple tiers, it is counted
102    /// multiple times. Use [`unique_count`](Self::unique_count) for deduplicated count.
103    #[must_use]
104    pub fn total_count(&self) -> usize {
105        self.user_specs.len() + self.computed.len() + self.defaults.len()
106    }
107
108    /// Get the number of unique functions with at least one summary.
109    #[must_use]
110    pub fn unique_count(&self) -> usize {
111        let mut ids: std::collections::BTreeSet<FunctionId> = std::collections::BTreeSet::new();
112        ids.extend(self.user_specs.keys());
113        ids.extend(self.computed.keys());
114        ids.extend(self.defaults.keys());
115        ids.len()
116    }
117
118    /// Check if the registry is empty (no summaries in any tier).
119    #[must_use]
120    pub fn is_empty(&self) -> bool {
121        self.user_specs.is_empty() && self.computed.is_empty() && self.defaults.is_empty()
122    }
123
124    /// Iterate over all unique function IDs, yielding the highest-priority
125    /// summary for each.
126    pub fn iter(&self) -> impl Iterator<Item = (&FunctionId, &FunctionSummary)> {
127        // Collect all unique IDs then look up with priority
128        let mut ids: std::collections::BTreeSet<&FunctionId> = std::collections::BTreeSet::new();
129        ids.extend(self.user_specs.keys());
130        ids.extend(self.computed.keys());
131        ids.extend(self.defaults.keys());
132
133        ids.into_iter()
134            .filter_map(move |id| self.get(id).map(|s| (id, s)))
135    }
136
137    /// Get the number of summaries in the computed tier.
138    #[must_use]
139    pub fn computed_count(&self) -> usize {
140        self.computed.len()
141    }
142
143    /// Get the number of summaries in the user tier.
144    #[must_use]
145    pub fn user_count(&self) -> usize {
146        self.user_specs.len()
147    }
148
149    /// Get the number of summaries in the defaults tier.
150    #[must_use]
151    pub fn defaults_count(&self) -> usize {
152        self.defaults.len()
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use crate::spec::{FunctionSpec, Nullness, Pointer, ReturnSpec, Role};
160    use crate::summary::{SummaryNullness, SummaryPrecision, SummaryRole, SummarySource};
161
162    fn make_fid(name: &str) -> FunctionId {
163        FunctionId::derive(name.as_bytes())
164    }
165
166    fn make_summary(name: &str, source: SummarySource) -> FunctionSummary {
167        let mut s = FunctionSummary::default_for(make_fid(name));
168        s.source = source;
169        s
170    }
171
172    #[test]
173    fn empty_registry() {
174        let reg = SummaryRegistry::new();
175        assert!(reg.is_empty());
176        assert_eq!(reg.total_count(), 0);
177        assert_eq!(reg.unique_count(), 0);
178        assert!(reg.get(&make_fid("malloc")).is_none());
179    }
180
181    #[test]
182    fn insert_and_get_computed() {
183        let mut reg = SummaryRegistry::new();
184        let summary = make_summary("test_fn", SummarySource::Analysis);
185        let fid = summary.function_id;
186
187        reg.insert_computed(summary);
188
189        let found = reg.get(&fid).expect("should find computed summary");
190        assert_eq!(found.function_id, fid);
191        assert_eq!(found.source, SummarySource::Analysis);
192        assert_eq!(reg.computed_count(), 1);
193    }
194
195    #[test]
196    fn priority_user_over_computed() {
197        let mut reg = SummaryRegistry::new();
198        let fid = make_fid("func");
199
200        let mut computed = FunctionSummary::default_for(fid);
201        computed.source = SummarySource::Analysis;
202        computed.pure = false;
203        reg.insert_computed(computed);
204
205        let mut user = FunctionSummary::default_for(fid);
206        user.source = SummarySource::Spec;
207        user.pure = true;
208        reg.insert_user(user);
209
210        let found = reg.get(&fid).expect("should find summary");
211        assert_eq!(found.source, SummarySource::Spec);
212        assert!(found.pure);
213    }
214
215    #[test]
216    fn priority_computed_over_default() {
217        let mut reg = SummaryRegistry::new();
218        let fid = make_fid("func");
219
220        let mut default_s = FunctionSummary::default_for(fid);
221        default_s.source = SummarySource::Default;
222        default_s.version = 1;
223        reg.insert_default(default_s);
224
225        let mut computed = FunctionSummary::default_for(fid);
226        computed.source = SummarySource::Analysis;
227        computed.version = 5;
228        reg.insert_computed(computed);
229
230        let found = reg.get(&fid).expect("should find summary");
231        assert_eq!(found.source, SummarySource::Analysis);
232        assert_eq!(found.version, 5);
233    }
234
235    #[test]
236    fn priority_user_over_all() {
237        let mut reg = SummaryRegistry::new();
238        let fid = make_fid("func");
239
240        reg.insert_default(make_summary("func", SummarySource::Default));
241        reg.insert_computed(make_summary("func", SummarySource::Analysis));
242
243        let mut user = FunctionSummary::default_for(fid);
244        user.source = SummarySource::Spec;
245        user.noreturn = true;
246        reg.insert_user(user);
247
248        let found = reg.get(&fid).expect("should find summary");
249        assert_eq!(found.source, SummarySource::Spec);
250        assert!(found.noreturn);
251    }
252
253    #[test]
254    fn unique_count_deduplicates() {
255        let mut reg = SummaryRegistry::new();
256
257        reg.insert_default(make_summary("func", SummarySource::Default));
258        reg.insert_computed(make_summary("func", SummarySource::Analysis));
259        reg.insert_user(make_summary("func", SummarySource::Spec));
260
261        // Same function in all three tiers
262        assert_eq!(reg.total_count(), 3);
263        assert_eq!(reg.unique_count(), 1);
264    }
265
266    #[test]
267    fn load_specs_populates_defaults() {
268        let mut spec_reg = SpecRegistry::new();
269        let mut malloc_spec = FunctionSpec::new("malloc");
270        malloc_spec.role = Some(Role::Allocator);
271        malloc_spec.returns = Some(ReturnSpec {
272            pointer: Some(Pointer::FreshHeap),
273            nullness: Some(Nullness::MaybeNull),
274            ..ReturnSpec::default()
275        });
276        spec_reg.add(malloc_spec).expect("add spec");
277
278        let mut free_spec = FunctionSpec::new("free");
279        free_spec.role = Some(Role::Deallocator);
280        spec_reg.add(free_spec).expect("add spec");
281
282        let mut reg = SummaryRegistry::new();
283        reg.load_specs(&spec_reg);
284
285        assert_eq!(reg.defaults_count(), 2);
286
287        let malloc_id = make_fid("malloc");
288        let found = reg.get(&malloc_id).expect("should find malloc");
289        assert_eq!(found.role, Some(SummaryRole::Allocator));
290        assert_eq!(found.source, SummarySource::Spec);
291        assert_eq!(found.return_nullness, Some(SummaryNullness::MaybeNull));
292        assert_eq!(found.precision, SummaryPrecision::Sound);
293
294        let free_id = make_fid("free");
295        let found = reg.get(&free_id).expect("should find free");
296        assert_eq!(found.role, Some(SummaryRole::Deallocator));
297    }
298
299    #[test]
300    fn computed_overrides_loaded_specs() {
301        let mut spec_reg = SpecRegistry::new();
302        let mut spec = FunctionSpec::new("my_func");
303        spec.pure = Some(true);
304        spec_reg.add(spec).expect("add spec");
305
306        let mut reg = SummaryRegistry::new();
307        reg.load_specs(&spec_reg);
308
309        // Now insert a computed summary that overrides the default
310        let fid = make_fid("my_func");
311        let mut computed = FunctionSummary::default_for(fid);
312        computed.source = SummarySource::Analysis;
313        computed.pure = false;
314        computed.version = 3;
315        reg.insert_computed(computed);
316
317        let found = reg.get(&fid).expect("should find summary");
318        assert_eq!(found.source, SummarySource::Analysis);
319        assert!(!found.pure);
320        assert_eq!(found.version, 3);
321    }
322
323    #[test]
324    fn iter_yields_highest_priority() {
325        let mut reg = SummaryRegistry::new();
326
327        // Two different functions
328        reg.insert_default(make_summary("a", SummarySource::Default));
329        reg.insert_computed(make_summary("b", SummarySource::Analysis));
330
331        // Function "a" also has a computed version
332        let mut computed_a = FunctionSummary::default_for(make_fid("a"));
333        computed_a.source = SummarySource::Analysis;
334        computed_a.version = 10;
335        reg.insert_computed(computed_a);
336
337        let results: Vec<_> = reg.iter().collect();
338        assert_eq!(results.len(), 2);
339
340        // Find "a" - should be computed (higher priority than default)
341        let a_id = make_fid("a");
342        let a_summary = results.iter().find(|(id, _)| **id == a_id).expect("find a");
343        assert_eq!(a_summary.1.source, SummarySource::Analysis);
344        assert_eq!(a_summary.1.version, 10);
345    }
346
347    #[test]
348    fn load_user_specs_goes_to_user_tier() {
349        let mut spec_reg = SpecRegistry::new();
350        let mut spec = FunctionSpec::new("custom_alloc");
351        spec.role = Some(Role::Allocator);
352        spec_reg.add(spec).expect("add spec");
353
354        let mut reg = SummaryRegistry::new();
355        reg.load_user_specs(&spec_reg);
356
357        assert_eq!(reg.user_count(), 1);
358        assert_eq!(reg.defaults_count(), 0);
359
360        let fid = make_fid("custom_alloc");
361        let found = reg.get(&fid).expect("should find");
362        assert_eq!(found.role, Some(SummaryRole::Allocator));
363    }
364
365    #[test]
366    fn replace_computed_summary() {
367        let mut reg = SummaryRegistry::new();
368        let fid = make_fid("evolving_func");
369
370        let mut v1 = FunctionSummary::default_for(fid);
371        v1.source = SummarySource::Analysis;
372        v1.version = 1;
373        v1.pure = false;
374        reg.insert_computed(v1);
375
376        let mut v2 = FunctionSummary::default_for(fid);
377        v2.source = SummarySource::Analysis;
378        v2.version = 2;
379        v2.pure = true;
380        reg.insert_computed(v2);
381
382        let found = reg.get(&fid).expect("should find");
383        assert_eq!(found.version, 2);
384        assert!(found.pure);
385        assert_eq!(reg.computed_count(), 1);
386    }
387}