ggen_core/codegen/
incremental_cache.rs

1use crate::graph::Graph;
2use crate::manifest::{GenerationRule, GgenManifest};
3use ggen_utils::error::Result;
4use sha2::{Digest, Sha256};
5use std::collections::HashMap;
6use std::fs;
7use std::path::PathBuf;
8
9pub struct IncrementalCache {
10    cache_dir: PathBuf,
11    manifest_hash: String,
12    ontology_hash: String,
13    rule_hashes: HashMap<String, String>,
14    inference_state_hash: String,
15}
16
17pub struct CacheInvalidation {
18    pub manifest_changed: bool,
19    pub ontology_changed: bool,
20    pub changed_rules: Vec<String>,
21    pub inference_state_changed: bool,
22    pub should_rerun_all: bool,
23}
24
25impl IncrementalCache {
26    pub fn new(cache_dir: PathBuf) -> Self {
27        Self {
28            cache_dir,
29            manifest_hash: String::new(),
30            ontology_hash: String::new(),
31            rule_hashes: HashMap::new(),
32            inference_state_hash: String::new(),
33        }
34    }
35
36    pub fn load_cache_state(&mut self) -> Result<()> {
37        let manifest_hash_path = self.cache_dir.join("manifest.sha256");
38        let ontology_hash_path = self.cache_dir.join("ontology.sha256");
39        let rules_hash_path = self.cache_dir.join("rules.sha256");
40        let inference_hash_path = self.cache_dir.join("inference_state.sha256");
41
42        if manifest_hash_path.exists() {
43            self.manifest_hash = fs::read_to_string(&manifest_hash_path)?;
44        }
45
46        if ontology_hash_path.exists() {
47            self.ontology_hash = fs::read_to_string(&ontology_hash_path)?;
48        }
49
50        if rules_hash_path.exists() {
51            let rules_content = fs::read_to_string(&rules_hash_path)?;
52            for line in rules_content.lines() {
53                if let Some((name, hash)) = line.split_once('=') {
54                    self.rule_hashes.insert(name.to_string(), hash.to_string());
55                }
56            }
57        }
58
59        if inference_hash_path.exists() {
60            self.inference_state_hash = fs::read_to_string(&inference_hash_path)?;
61        }
62
63        Ok(())
64    }
65
66    pub fn save_cache_state(
67        &self, manifest: &GgenManifest, ontology_content: &str, inference_graph: &Graph,
68    ) -> Result<()> {
69        fs::create_dir_all(&self.cache_dir)?;
70
71        // Save manifest hash
72        let manifest_hash = Self::hash_manifest(manifest);
73        fs::write(self.cache_dir.join("manifest.sha256"), &manifest_hash)?;
74
75        // Save ontology hash
76        let ontology_hash = Self::hash_string(ontology_content);
77        fs::write(self.cache_dir.join("ontology.sha256"), &ontology_hash)?;
78
79        // Save rule hashes
80        let mut rules_content = String::new();
81        for rule in &manifest.generation.rules {
82            let rule_hash = Self::hash_generation_rule(rule);
83            rules_content.push_str(&format!("{}={}\n", rule.name, rule_hash));
84        }
85        fs::write(self.cache_dir.join("rules.sha256"), &rules_content)?;
86
87        // Save inference state hash
88        let inference_hash = Self::hash_graph_state(inference_graph);
89        fs::write(
90            self.cache_dir.join("inference_state.sha256"),
91            &inference_hash,
92        )?;
93
94        Ok(())
95    }
96
97    pub fn check_invalidation(
98        &self, manifest: &GgenManifest, ontology_content: &str, inference_graph: &Graph,
99    ) -> CacheInvalidation {
100        let manifest_changed = self.manifest_hash != Self::hash_manifest(manifest);
101        let ontology_changed = self.ontology_hash != Self::hash_string(ontology_content);
102        let inference_state_changed =
103            self.inference_state_hash != Self::hash_graph_state(inference_graph);
104
105        let mut changed_rules = vec![];
106        for rule in &manifest.generation.rules {
107            let current_hash = Self::hash_generation_rule(rule);
108            let empty_hash = String::new();
109            let cached_hash = self.rule_hashes.get(&rule.name).unwrap_or(&empty_hash);
110
111            if current_hash != *cached_hash {
112                changed_rules.push(rule.name.clone());
113            }
114        }
115
116        let should_rerun_all = manifest_changed || ontology_changed || inference_state_changed;
117
118        CacheInvalidation {
119            manifest_changed,
120            ontology_changed,
121            changed_rules,
122            inference_state_changed,
123            should_rerun_all,
124        }
125    }
126
127    pub fn get_rules_to_rerun(
128        &self, manifest: &GgenManifest, invalidation: &CacheInvalidation,
129        rule_dependencies: &HashMap<String, Vec<String>>,
130    ) -> Vec<String> {
131        if invalidation.should_rerun_all {
132            return manifest
133                .generation
134                .rules
135                .iter()
136                .map(|r| r.name.clone())
137                .collect();
138        }
139
140        let mut to_rerun = invalidation.changed_rules.clone();
141        let mut visited = std::collections::HashSet::new();
142
143        // Propagate changes to dependent rules
144        for rule_name in invalidation.changed_rules.iter() {
145            Self::propagate_changes(rule_name, rule_dependencies, &mut to_rerun, &mut visited);
146        }
147
148        to_rerun
149    }
150
151    fn propagate_changes(
152        rule_name: &str, dependencies: &HashMap<String, Vec<String>>, to_rerun: &mut Vec<String>,
153        visited: &mut std::collections::HashSet<String>,
154    ) {
155        if visited.contains(rule_name) {
156            return;
157        }
158
159        visited.insert(rule_name.to_string());
160
161        // Find all rules that depend on this rule
162        for (dependent, deps) in dependencies {
163            if deps.contains(&rule_name.to_string()) && !to_rerun.contains(dependent) {
164                to_rerun.push(dependent.clone());
165                Self::propagate_changes(dependent, dependencies, to_rerun, visited);
166            }
167        }
168    }
169
170    fn hash_manifest(manifest: &GgenManifest) -> String {
171        let content = format!(
172            "{:?}{:?}{:?}{:?}",
173            manifest.project, manifest.ontology, manifest.inference, manifest.generation,
174        );
175        Self::hash_string(&content)
176    }
177
178    fn hash_generation_rule(rule: &GenerationRule) -> String {
179        let content = format!("{:?}", rule);
180        Self::hash_string(&content)
181    }
182
183    fn hash_graph_state(graph: &Graph) -> String {
184        let content = format!("{} triples", graph.len());
185        Self::hash_string(&content)
186    }
187
188    fn hash_string(content: &str) -> String {
189        let mut hasher = Sha256::new();
190        hasher.update(content.as_bytes());
191        format!("{:x}", hasher.finalize())
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_hash_consistency() {
201        let content = "test content";
202        let hash1 = IncrementalCache::hash_string(content);
203        let hash2 = IncrementalCache::hash_string(content);
204        assert_eq!(hash1, hash2);
205    }
206
207    #[test]
208    fn test_hash_difference() {
209        let content1 = "test content 1";
210        let content2 = "test content 2";
211        let hash1 = IncrementalCache::hash_string(content1);
212        let hash2 = IncrementalCache::hash_string(content2);
213        assert_ne!(hash1, hash2);
214    }
215}