armature_rhai/
script.rs

1//! Script loading and caching.
2
3use crate::error::{Result, RhaiError};
4use dashmap::DashMap;
5use rhai::AST;
6use std::collections::HashMap;
7use std::fs;
8use std::path::{Path, PathBuf};
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::Arc;
11use std::time::SystemTime;
12
13/// A compiled Rhai script.
14#[derive(Debug)]
15pub struct CompiledScript {
16    /// Path to the script file.
17    pub path: PathBuf,
18    /// Compiled AST.
19    ast: AST,
20    /// Compilation timestamp.
21    compiled_at: SystemTime,
22}
23
24impl CompiledScript {
25    /// Create a new compiled script.
26    pub fn new(path: PathBuf, ast: AST) -> Self {
27        Self {
28            path,
29            ast,
30            compiled_at: SystemTime::now(),
31        }
32    }
33
34    /// Get the compiled AST.
35    pub fn ast(&self) -> &AST {
36        &self.ast
37    }
38
39    /// Get the compilation timestamp.
40    pub fn compiled_at(&self) -> SystemTime {
41        self.compiled_at
42    }
43
44    /// Check if the script is stale (source file modified).
45    pub fn is_stale(&self) -> bool {
46        if let Ok(metadata) = fs::metadata(&self.path) {
47            if let Ok(modified) = metadata.modified() {
48                return modified > self.compiled_at;
49            }
50        }
51        false
52    }
53}
54
55/// Cache for compiled scripts.
56pub struct ScriptCache {
57    scripts: HashMap<PathBuf, Arc<CompiledScript>>,
58    hits: AtomicU64,
59    misses: AtomicU64,
60}
61
62impl Default for ScriptCache {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68impl ScriptCache {
69    /// Create a new empty cache.
70    pub fn new() -> Self {
71        Self {
72            scripts: HashMap::new(),
73            hits: AtomicU64::new(0),
74            misses: AtomicU64::new(0),
75        }
76    }
77
78    /// Get a cached script.
79    pub fn get(&self, path: &Path) -> Option<Arc<CompiledScript>> {
80        if let Some(script) = self.scripts.get(path) {
81            self.hits.fetch_add(1, Ordering::Relaxed);
82            Some(script.clone())
83        } else {
84            self.misses.fetch_add(1, Ordering::Relaxed);
85            None
86        }
87    }
88
89    /// Insert a script into the cache.
90    pub fn insert(&mut self, path: PathBuf, script: Arc<CompiledScript>) {
91        self.scripts.insert(path, script);
92    }
93
94    /// Remove a script from the cache.
95    pub fn remove(&mut self, path: &Path) -> Option<Arc<CompiledScript>> {
96        self.scripts.remove(path)
97    }
98
99    /// Clear all cached scripts.
100    pub fn clear(&mut self) {
101        self.scripts.clear();
102    }
103
104    /// Get cache statistics.
105    pub fn stats(&self) -> crate::engine::CacheStats {
106        crate::engine::CacheStats {
107            cached_scripts: self.scripts.len(),
108            hits: self.hits.load(Ordering::Relaxed),
109            misses: self.misses.load(Ordering::Relaxed),
110        }
111    }
112
113    /// Remove stale scripts from the cache.
114    pub fn evict_stale(&mut self) -> Vec<PathBuf> {
115        let stale: Vec<PathBuf> = self
116            .scripts
117            .iter()
118            .filter(|(_, script)| script.is_stale())
119            .map(|(path, _)| path.clone())
120            .collect();
121
122        for path in &stale {
123            self.scripts.remove(path);
124        }
125
126        stale
127    }
128}
129
130/// Concurrent script cache for multi-threaded access.
131pub struct ConcurrentScriptCache {
132    scripts: DashMap<PathBuf, Arc<CompiledScript>>,
133    hits: AtomicU64,
134    misses: AtomicU64,
135}
136
137impl Default for ConcurrentScriptCache {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143impl ConcurrentScriptCache {
144    /// Create a new concurrent cache.
145    pub fn new() -> Self {
146        Self {
147            scripts: DashMap::new(),
148            hits: AtomicU64::new(0),
149            misses: AtomicU64::new(0),
150        }
151    }
152
153    /// Get a cached script.
154    pub fn get(&self, path: &Path) -> Option<Arc<CompiledScript>> {
155        if let Some(script) = self.scripts.get(path) {
156            self.hits.fetch_add(1, Ordering::Relaxed);
157            Some(script.clone())
158        } else {
159            self.misses.fetch_add(1, Ordering::Relaxed);
160            None
161        }
162    }
163
164    /// Insert a script into the cache.
165    pub fn insert(&self, path: PathBuf, script: Arc<CompiledScript>) {
166        self.scripts.insert(path, script);
167    }
168
169    /// Remove a script from the cache.
170    pub fn remove(&self, path: &Path) -> Option<Arc<CompiledScript>> {
171        self.scripts.remove(path).map(|(_, v)| v)
172    }
173
174    /// Clear all cached scripts.
175    pub fn clear(&self) {
176        self.scripts.clear();
177    }
178}
179
180/// Script loader for file operations.
181pub struct ScriptLoader {
182    /// Base directory for scripts.
183    base_dir: PathBuf,
184}
185
186impl ScriptLoader {
187    /// Create a new script loader.
188    pub fn new(base_dir: impl Into<PathBuf>) -> Self {
189        Self {
190            base_dir: base_dir.into(),
191        }
192    }
193
194    /// Resolve a script path relative to the base directory.
195    pub fn resolve_path(&self, path: &Path) -> PathBuf {
196        if path.is_absolute() {
197            path.to_path_buf()
198        } else {
199            self.base_dir.join(path)
200        }
201    }
202
203    /// Load a script file.
204    pub fn load(&self, path: &Path) -> Result<String> {
205        let full_path = self.resolve_path(path);
206
207        if !full_path.exists() {
208            return Err(RhaiError::ScriptNotFound { path: full_path });
209        }
210
211        fs::read_to_string(&full_path).map_err(RhaiError::from)
212    }
213
214    /// Check if a script exists.
215    pub fn exists(&self, path: &Path) -> bool {
216        self.resolve_path(path).exists()
217    }
218
219    /// List all script files in a directory.
220    pub fn list_scripts(&self, dir: &Path, extension: &str) -> Result<Vec<PathBuf>> {
221        let full_path = self.resolve_path(dir);
222        let mut scripts = Vec::new();
223
224        if !full_path.exists() {
225            return Ok(scripts);
226        }
227
228        self.collect_scripts(&full_path, extension, &mut scripts)?;
229        Ok(scripts)
230    }
231
232    fn collect_scripts(
233        &self,
234        dir: &Path,
235        extension: &str,
236        scripts: &mut Vec<PathBuf>,
237    ) -> Result<()> {
238        for entry in fs::read_dir(dir)? {
239            let entry = entry?;
240            let path = entry.path();
241
242            if path.is_dir() {
243                self.collect_scripts(&path, extension, scripts)?;
244            } else if path.extension().map(|e| e == extension).unwrap_or(false) {
245                scripts.push(path);
246            }
247        }
248        Ok(())
249    }
250
251    /// Get the base directory.
252    pub fn base_dir(&self) -> &Path {
253        &self.base_dir
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use std::io::Write;
261    use tempfile::TempDir;
262
263    #[test]
264    fn test_script_loader() {
265        let temp = TempDir::new().unwrap();
266        let mut file = fs::File::create(temp.path().join("test.rhai")).unwrap();
267        writeln!(file, "let x = 42;").unwrap();
268
269        let loader = ScriptLoader::new(temp.path());
270        let content = loader.load(Path::new("test.rhai")).unwrap();
271        assert!(content.contains("42"));
272    }
273
274    #[test]
275    fn test_script_cache() {
276        use rhai::Engine;
277
278        let engine = Engine::new();
279        let ast = engine.compile("let x = 1;").unwrap();
280        let script = Arc::new(CompiledScript::new(PathBuf::from("test.rhai"), ast));
281
282        let mut cache = ScriptCache::new();
283        cache.insert(PathBuf::from("test.rhai"), script.clone());
284
285        assert!(cache.get(Path::new("test.rhai")).is_some());
286        assert!(cache.get(Path::new("other.rhai")).is_none());
287
288        let stats = cache.stats();
289        assert_eq!(stats.cached_scripts, 1);
290        assert_eq!(stats.hits, 1);
291        assert_eq!(stats.misses, 1);
292    }
293}
294