Skip to main content

weaveffi_core/
codegen.rs

1use anyhow::Result;
2use camino::Utf8Path;
3use weaveffi_ir::ir::Api;
4
5use crate::cache;
6use crate::config::GeneratorConfig;
7
8pub trait Generator {
9    fn name(&self) -> &'static str;
10    fn generate(&self, api: &Api, out_dir: &Utf8Path) -> Result<()>;
11
12    fn generate_with_config(
13        &self,
14        api: &Api,
15        out_dir: &Utf8Path,
16        _config: &GeneratorConfig,
17    ) -> Result<()> {
18        self.generate(api, out_dir)
19    }
20
21    fn output_files(&self, _api: &Api, _out_dir: &Utf8Path) -> Vec<String> {
22        vec![]
23    }
24}
25
26#[derive(Default)]
27pub struct Orchestrator<'a> {
28    generators: Vec<&'a dyn Generator>,
29}
30
31impl<'a> Orchestrator<'a> {
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    pub fn with_generator(mut self, gen: &'a dyn Generator) -> Self {
37        self.generators.push(gen);
38        self
39    }
40
41    pub fn run(
42        &self,
43        api: &Api,
44        out_dir: &Utf8Path,
45        config: &GeneratorConfig,
46        force: bool,
47    ) -> Result<()> {
48        let hash = cache::hash_api(api);
49
50        if !force {
51            if let Some(cached) = cache::read_cache(out_dir) {
52                if cached == hash {
53                    println!("No changes detected, skipping code generation.");
54                    return Ok(());
55                }
56            }
57        }
58
59        for g in &self.generators {
60            g.generate_with_config(api, out_dir, config)?;
61        }
62
63        cache::write_cache(out_dir, &hash)?;
64        Ok(())
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use std::sync::atomic::{AtomicUsize, Ordering};
72    use std::sync::Arc;
73    use weaveffi_ir::ir::{Function, Module, Param, TypeRef};
74
75    struct CountingGenerator {
76        calls: Arc<AtomicUsize>,
77    }
78
79    impl Generator for CountingGenerator {
80        fn name(&self) -> &'static str {
81            "counting"
82        }
83
84        fn generate(&self, _api: &Api, out_dir: &Utf8Path) -> Result<()> {
85            self.calls.fetch_add(1, Ordering::SeqCst);
86            std::fs::write(out_dir.join("output.txt").as_std_path(), "generated")?;
87            Ok(())
88        }
89    }
90
91    fn test_api() -> Api {
92        Api {
93            version: "0.1.0".to_string(),
94            modules: vec![Module {
95                name: "math".to_string(),
96                functions: vec![Function {
97                    name: "add".to_string(),
98                    params: vec![
99                        Param {
100                            name: "a".to_string(),
101                            ty: TypeRef::I32,
102                        },
103                        Param {
104                            name: "b".to_string(),
105                            ty: TypeRef::I32,
106                        },
107                    ],
108                    returns: Some(TypeRef::I32),
109                    doc: None,
110                    r#async: false,
111                }],
112                structs: vec![],
113                enums: vec![],
114                errors: None,
115            }],
116        }
117    }
118
119    #[test]
120    fn incremental_skips_when_unchanged() {
121        let dir = tempfile::tempdir().unwrap();
122        let out_dir = Utf8Path::from_path(dir.path()).unwrap();
123        let api = test_api();
124        let config = GeneratorConfig::default();
125        let calls = Arc::new(AtomicUsize::new(0));
126        let gen = CountingGenerator {
127            calls: Arc::clone(&calls),
128        };
129
130        let orch = Orchestrator::new().with_generator(&gen);
131
132        orch.run(&api, out_dir, &config, false).unwrap();
133        assert_eq!(calls.load(Ordering::SeqCst), 1);
134        let content_after_first = std::fs::read_to_string(out_dir.join("output.txt")).unwrap();
135
136        orch.run(&api, out_dir, &config, false).unwrap();
137        assert_eq!(
138            calls.load(Ordering::SeqCst),
139            1,
140            "generator should not run again"
141        );
142        let content_after_second = std::fs::read_to_string(out_dir.join("output.txt")).unwrap();
143
144        assert_eq!(content_after_first, content_after_second);
145    }
146
147    #[test]
148    fn force_bypasses_cache() {
149        let dir = tempfile::tempdir().unwrap();
150        let out_dir = Utf8Path::from_path(dir.path()).unwrap();
151        let api = test_api();
152        let config = GeneratorConfig::default();
153        let calls = Arc::new(AtomicUsize::new(0));
154        let gen = CountingGenerator {
155            calls: Arc::clone(&calls),
156        };
157
158        let orch = Orchestrator::new().with_generator(&gen);
159
160        orch.run(&api, out_dir, &config, false).unwrap();
161        assert_eq!(calls.load(Ordering::SeqCst), 1);
162
163        orch.run(&api, out_dir, &config, true).unwrap();
164        assert_eq!(calls.load(Ordering::SeqCst), 2, "force should bypass cache");
165    }
166}