Skip to main content

intent_ir/
incremental.rs

1//! Incremental verification.
2//!
3//! Caches per-item content hashes and verification results so that
4//! only changed items (and their dependents) are re-verified.
5
6use std::collections::{HashMap, HashSet};
7use std::hash::{Hash, Hasher};
8
9use serde::{Deserialize, Serialize};
10
11use crate::types::*;
12use crate::verify::{
13    Obligation, VerifyError, analyze_obligations, collect_module_call_names, verify_edge_guard,
14    verify_function, verify_invariant,
15};
16
17/// Cached verification state for a module.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct VerifyCache {
20    /// Module name.
21    pub module_name: String,
22    /// Hash of the global context (sorted struct + function names).
23    /// If this changes, the entire module must be re-verified.
24    pub context_hash: u64,
25    /// Per-struct content hashes (structs don't have direct errors but
26    /// changes invalidate dependent functions/invariants).
27    pub structs: HashMap<String, u64>,
28    /// Per-function content hashes and cached errors.
29    pub functions: HashMap<String, CachedItem>,
30    /// Per-invariant content hashes and cached errors.
31    pub invariants: HashMap<String, CachedItem>,
32    /// Combined hash and errors for all edge guards.
33    pub edge_guards: CachedItem,
34}
35
36/// Cached verification result for a single item.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct CachedItem {
39    pub content_hash: u64,
40    pub errors: Vec<VerifyError>,
41}
42
43/// Result of incremental verification.
44#[derive(Debug, Clone, Serialize)]
45pub struct IncrementalResult {
46    /// All verification errors (merged from cached + fresh).
47    pub errors: Vec<VerifyError>,
48    /// All obligations (always recomputed).
49    pub obligations: Vec<Obligation>,
50    /// Updated cache to persist.
51    pub cache: VerifyCache,
52    /// Statistics about what was re-verified vs cached.
53    pub stats: IncrementalStats,
54}
55
56/// Statistics for incremental verification.
57#[derive(Debug, Clone, Serialize)]
58pub struct IncrementalStats {
59    pub total_items: usize,
60    pub reverified: usize,
61    pub cached: usize,
62    /// True if the global context changed, forcing full re-verification.
63    pub full_reverify: bool,
64}
65
66/// Perform incremental verification of an IR module.
67///
68/// If `cache` is `None`, performs a full verification and builds a new cache.
69/// If `cache` is `Some`, compares content hashes to determine which items
70/// changed and only re-verifies those.
71///
72/// Obligations are always recomputed since they are cross-cutting.
73pub fn incremental_verify(module: &Module, cache: Option<&VerifyCache>) -> IncrementalResult {
74    let current_ctx = context_hash(module);
75    let total_items = module.functions.len()
76        + module.invariants.len()
77        + if module.edge_guards.is_empty() { 0 } else { 1 };
78
79    // Build the global context needed by per-item verifiers.
80    let known_types: HashSet<&str> = module
81        .structs
82        .iter()
83        .map(|s| s.name.as_str())
84        .chain(module.functions.iter().map(|f| f.name.as_str()))
85        .collect();
86    let mut call_names = HashSet::new();
87    collect_module_call_names(module, &mut call_names);
88
89    // Check if we can use the cache.
90    let cache_valid = cache
91        .map(|c| c.module_name == module.name && c.context_hash == current_ctx)
92        .unwrap_or(false);
93
94    let mut errors = Vec::new();
95    let mut reverified = 0;
96    let mut cached_count = 0;
97
98    // -- Per-item hashes --
99    let mut new_struct_hashes = HashMap::new();
100    let mut new_func_items = HashMap::new();
101    let mut new_inv_items = HashMap::new();
102
103    // Compute struct hashes and check for changes.
104    let mut struct_changed = false;
105    for s in &module.structs {
106        let h = content_hash_struct(s);
107        if cache_valid {
108            if let Some(old_h) = cache.and_then(|c| c.structs.get(&s.name)) {
109                if *old_h != h {
110                    struct_changed = true;
111                }
112            } else {
113                struct_changed = true;
114            }
115        }
116        new_struct_hashes.insert(s.name.clone(), h);
117    }
118    // If structs were removed, that's also a change.
119    if cache_valid && let Some(c) = cache {
120        for name in c.structs.keys() {
121            if !new_struct_hashes.contains_key(name) {
122                struct_changed = true;
123            }
124        }
125    }
126
127    // Verify functions.
128    for func in &module.functions {
129        let h = content_hash_function(func);
130        let use_cached = cache_valid
131            && !struct_changed
132            && cache
133                .and_then(|c| c.functions.get(&func.name))
134                .map(|ci| ci.content_hash == h)
135                .unwrap_or(false);
136
137        if use_cached {
138            let cached_errors = &cache.unwrap().functions[&func.name].errors;
139            errors.extend(cached_errors.iter().cloned());
140            cached_count += 1;
141            new_func_items.insert(
142                func.name.clone(),
143                CachedItem {
144                    content_hash: h,
145                    errors: cached_errors.clone(),
146                },
147            );
148        } else {
149            let mut item_errors = Vec::new();
150            verify_function(func, &known_types, &call_names, &mut item_errors);
151            errors.extend(item_errors.iter().cloned());
152            reverified += 1;
153            new_func_items.insert(
154                func.name.clone(),
155                CachedItem {
156                    content_hash: h,
157                    errors: item_errors,
158                },
159            );
160        }
161    }
162
163    // Verify invariants.
164    for inv in &module.invariants {
165        let h = content_hash_invariant(inv);
166        let use_cached = cache_valid
167            && !struct_changed
168            && cache
169                .and_then(|c| c.invariants.get(&inv.name))
170                .map(|ci| ci.content_hash == h)
171                .unwrap_or(false);
172
173        if use_cached {
174            let cached_errors = &cache.unwrap().invariants[&inv.name].errors;
175            errors.extend(cached_errors.iter().cloned());
176            cached_count += 1;
177            new_inv_items.insert(
178                inv.name.clone(),
179                CachedItem {
180                    content_hash: h,
181                    errors: cached_errors.clone(),
182                },
183            );
184        } else {
185            let mut item_errors = Vec::new();
186            verify_invariant(inv, &known_types, &call_names, &mut item_errors);
187            errors.extend(item_errors.iter().cloned());
188            reverified += 1;
189            new_inv_items.insert(
190                inv.name.clone(),
191                CachedItem {
192                    content_hash: h,
193                    errors: item_errors,
194                },
195            );
196        }
197    }
198
199    // Verify edge guards (treated as a single unit).
200    let eg_hash = content_hash_edge_guards(&module.edge_guards);
201    let eg_use_cached = cache_valid
202        && !struct_changed
203        && cache
204            .map(|c| c.edge_guards.content_hash == eg_hash)
205            .unwrap_or(false);
206    let new_eg = if !module.edge_guards.is_empty() {
207        if eg_use_cached {
208            let cached_errors = &cache.unwrap().edge_guards.errors;
209            errors.extend(cached_errors.iter().cloned());
210            cached_count += 1;
211            CachedItem {
212                content_hash: eg_hash,
213                errors: cached_errors.clone(),
214            }
215        } else {
216            let mut item_errors = Vec::new();
217            for guard in &module.edge_guards {
218                verify_edge_guard(guard, &known_types, &mut item_errors);
219            }
220            errors.extend(item_errors.iter().cloned());
221            if total_items > 0 {
222                reverified += 1;
223            }
224            CachedItem {
225                content_hash: eg_hash,
226                errors: item_errors,
227            }
228        }
229    } else {
230        CachedItem {
231            content_hash: 0,
232            errors: vec![],
233        }
234    };
235
236    // Always recompute obligations (cheap, cross-cutting).
237    let obligations = analyze_obligations(module);
238
239    let new_cache = VerifyCache {
240        module_name: module.name.clone(),
241        context_hash: current_ctx,
242        structs: new_struct_hashes,
243        functions: new_func_items,
244        invariants: new_inv_items,
245        edge_guards: new_eg,
246    };
247
248    IncrementalResult {
249        errors,
250        obligations,
251        cache: new_cache,
252        stats: IncrementalStats {
253            total_items,
254            reverified,
255            cached: cached_count,
256            full_reverify: !cache_valid || struct_changed,
257        },
258    }
259}
260
261// ── Hashing helpers ────────────────────────────────────────
262
263/// Hash of the global context: sorted struct + function names.
264/// If items are added or removed, this changes.
265fn context_hash(module: &Module) -> u64 {
266    let mut names: Vec<&str> = module
267        .structs
268        .iter()
269        .map(|s| s.name.as_str())
270        .chain(module.functions.iter().map(|f| f.name.as_str()))
271        .chain(module.invariants.iter().map(|i| i.name.as_str()))
272        .collect();
273    names.sort();
274    hash_parts(&names)
275}
276
277/// Content hash for a struct (name + fields), excluding source traces.
278fn content_hash_struct(s: &Struct) -> u64 {
279    let fields_json: Vec<String> = s
280        .fields
281        .iter()
282        .map(|f| format!("{}:{}", f.name, serde_json::to_string(&f.ty).unwrap()))
283        .collect();
284    let mut parts = vec![s.name.clone()];
285    parts.extend(fields_json);
286    let refs: Vec<&str> = parts.iter().map(|s| s.as_str()).collect();
287    hash_parts(&refs)
288}
289
290/// Content hash for a function (name + params + pre/postconditions + properties),
291/// excluding source traces.
292fn content_hash_function(func: &Function) -> u64 {
293    let mut parts = vec![func.name.clone()];
294    for p in &func.params {
295        parts.push(format!(
296            "param:{}:{}",
297            p.name,
298            serde_json::to_string(&p.ty).unwrap()
299        ));
300    }
301    for pre in &func.preconditions {
302        parts.push(format!("pre:{}", expr_signature(&pre.expr)));
303    }
304    for post in &func.postconditions {
305        match post {
306            Postcondition::Always { expr, .. } => {
307                parts.push(format!("post:{}", expr_signature(expr)));
308            }
309            Postcondition::When { guard, expr, .. } => {
310                parts.push(format!(
311                    "post-when:{}:{}",
312                    expr_signature(guard),
313                    expr_signature(expr)
314                ));
315            }
316        }
317    }
318    for prop in &func.properties {
319        parts.push(format!(
320            "prop:{}:{}",
321            prop.key,
322            serde_json::to_string(&prop.value).unwrap()
323        ));
324    }
325    let refs: Vec<&str> = parts.iter().map(|s| s.as_str()).collect();
326    hash_parts(&refs)
327}
328
329/// Content hash for an invariant (name + expression), excluding source traces.
330fn content_hash_invariant(inv: &Invariant) -> u64 {
331    let parts = [inv.name.as_str(), &expr_signature(&inv.expr)];
332    hash_parts(&parts)
333}
334
335/// Content hash for all edge guards combined.
336fn content_hash_edge_guards(guards: &[EdgeGuard]) -> u64 {
337    let mut parts = Vec::new();
338    for g in guards {
339        parts.push(format!(
340            "guard:{}:{}",
341            expr_signature(&g.condition),
342            g.action
343        ));
344        for (k, v) in &g.args {
345            parts.push(format!("arg:{}:{}", k, expr_signature(v)));
346        }
347    }
348    let refs: Vec<&str> = parts.iter().map(|s| s.as_str()).collect();
349    hash_parts(&refs)
350}
351
352/// Produce a canonical string representation of an IR expression (no traces).
353fn expr_signature(expr: &IrExpr) -> String {
354    serde_json::to_string(expr).unwrap()
355}
356
357/// Hash a slice of string parts using DefaultHasher.
358fn hash_parts(parts: &[&str]) -> u64 {
359    let mut hasher = std::collections::hash_map::DefaultHasher::new();
360    for part in parts {
361        part.hash(&mut hasher);
362    }
363    hasher.finish()
364}