oxur_repl/compiler/
cached.rs

1//! Cached compilation of Rust code to dynamic libraries
2//!
3//! Manages the full compilation pipeline with caching for performance.
4//!
5//! Based on ODD-0026 Section 2.1
6
7use crate::cache::ArtifactCache;
8use crate::session::SessionDir;
9use std::path::PathBuf;
10use std::process::Command;
11use std::sync::Arc;
12use thiserror::Error;
13
14/// Compilation errors
15#[derive(Debug, Error)]
16pub enum CompilerError {
17    #[error("Failed to write source file: {0}")]
18    WriteSourceFailed(#[from] std::io::Error),
19
20    #[error("Session directory error: {0}")]
21    SessionDirError(String),
22
23    #[error("Compilation failed: {0}")]
24    CompilationFailed(String),
25
26    #[error("Cache operation failed: {0}")]
27    CacheFailed(String),
28
29    #[error("Failed to find rustc: {0}")]
30    RustcNotFound(String),
31}
32
33impl From<crate::session::SessionDirError> for CompilerError {
34    fn from(err: crate::session::SessionDirError) -> Self {
35        CompilerError::SessionDirError(err.to_string())
36    }
37}
38
39pub type Result<T> = std::result::Result<T, CompilerError>;
40
41/// Cached compiler for Rust code
42///
43/// Manages compilation with SHA256-based caching to avoid recompiling
44/// identical code.
45///
46/// # Examples
47///
48/// ```no_run
49/// use oxur_repl::compiler::CachedCompiler;
50/// use oxur_repl::cache::ArtifactCache;
51/// use oxur_repl::session::SessionDir;
52/// use oxur_repl::protocol::SessionId;
53/// use std::sync::Arc;
54///
55/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
56/// let cache = ArtifactCache::new()?;
57/// let session_id = SessionId::new("test");
58/// let session_dir = Arc::new(SessionDir::new(&session_id)?);
59///
60/// let mut compiler = CachedCompiler::new(cache, session_dir);
61///
62/// let source = "pub extern \"C\" fn oxur_eval_test() { println!(\"Hello!\"); }";
63/// let lib_path = compiler.compile("test", source, 2)?;
64///
65/// println!("Compiled to: {}", lib_path.display());
66/// # Ok(())
67/// # }
68/// ```
69#[derive(Debug, Clone)]
70pub struct CachedCompiler {
71    /// Artifact cache for storing compiled libraries
72    cache: ArtifactCache,
73
74    /// Session directory for temporary files (shared via Arc)
75    session_dir: Arc<SessionDir>,
76}
77
78impl CachedCompiler {
79    /// Create a new cached compiler
80    ///
81    /// # Arguments
82    ///
83    /// * `cache` - Artifact cache for storing compiled libraries
84    /// * `session_dir` - Session directory for temporary compilation files (shared via Arc)
85    pub fn new(cache: ArtifactCache, session_dir: Arc<SessionDir>) -> Self {
86        Self { cache, session_dir }
87    }
88
89    /// Compile Rust source code to a dynamic library
90    ///
91    /// Uses SHA256-based caching to avoid recompiling identical code.
92    ///
93    /// # Arguments
94    ///
95    /// * `cache_key` - Cache key for this compilation unit
96    /// * `source` - Rust source code to compile
97    /// * `opt_level` - Optimization level (0-3)
98    ///
99    /// # Returns
100    ///
101    /// Path to the compiled dynamic library (.so/.dylib/.dll)
102    ///
103    /// # Errors
104    ///
105    /// Returns error if:
106    /// - Source file cannot be written
107    /// - Compilation fails
108    /// - Cache operations fail
109    pub fn compile(
110        &mut self,
111        cache_key: impl AsRef<str>,
112        source: impl AsRef<str>,
113        opt_level: u8,
114    ) -> Result<PathBuf> {
115        let cache_key = cache_key.as_ref();
116        let source = source.as_ref();
117
118        // Generate cache key from source and compilation options
119        let content_key = self.cache.generate_key(
120            source,
121            &[], // TODO: Track dependencies when needed
122            opt_level,
123            "default", // TODO: Add source map configuration
124        );
125
126        // Check if already in cache
127        if let Some(cached_path) =
128            self.cache.get(&content_key).map_err(|e| CompilerError::CacheFailed(e.to_string()))?
129        {
130            return Ok(cached_path);
131        }
132
133        // Not in cache - need to compile
134        let lib_path = self.compile_to_dylib(cache_key, source, opt_level)?;
135
136        // Insert into cache and return cached path
137        let cached_path = self
138            .cache
139            .insert(&content_key, &lib_path)
140            .map_err(|e| CompilerError::CacheFailed(e.to_string()))?;
141
142        Ok(cached_path)
143    }
144
145    /// Compile source to dynamic library
146    fn compile_to_dylib(&self, cache_key: &str, source: &str, opt_level: u8) -> Result<PathBuf> {
147        // Write source to file
148        let source_path = self.session_dir.write_source(format!("{}.rs", cache_key), source)?;
149
150        // Determine output library name based on platform
151        #[cfg(target_os = "macos")]
152        let lib_name = format!("lib{}.dylib", cache_key);
153
154        #[cfg(target_os = "linux")]
155        let lib_name = format!("lib{}.so", cache_key);
156
157        #[cfg(target_os = "windows")]
158        let lib_name = format!("{}.dll", cache_key);
159
160        let lib_path = self.session_dir.path().join(&lib_name);
161
162        // Compile using rustc with JSON error format for better error messages
163        let output = Command::new("rustc")
164            .arg("--crate-type=cdylib")
165            .arg(format!("-Copt-level={}", opt_level))
166            .arg("--error-format=json")
167            .arg("-o")
168            .arg(&lib_path)
169            .arg(&source_path)
170            .output()
171            .map_err(|e| CompilerError::RustcNotFound(e.to_string()))?;
172
173        if !output.status.success() {
174            let stderr = String::from_utf8_lossy(&output.stderr);
175
176            // Try to parse and translate errors
177            use crate::compiler::ErrorTranslator;
178            let translator = ErrorTranslator::new();
179
180            match translator.parse_and_translate(&stderr) {
181                Ok(diagnostics) if !diagnostics.is_empty() => {
182                    // Format translated errors
183                    let formatted_errors: Vec<String> =
184                        diagnostics.iter().map(|d| d.format()).collect();
185                    return Err(CompilerError::CompilationFailed(formatted_errors.join("\n")));
186                }
187                _ => {
188                    // Fallback to raw stderr if parsing fails
189                    return Err(CompilerError::CompilationFailed(stderr.to_string()));
190                }
191            }
192        }
193
194        Ok(lib_path)
195    }
196
197    /// Get the artifact cache
198    pub fn cache(&self) -> &ArtifactCache {
199        &self.cache
200    }
201
202    /// Get a mutable reference to the artifact cache
203    pub fn cache_mut(&mut self) -> &mut ArtifactCache {
204        &mut self.cache
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::protocol::SessionId;
212    use std::env;
213
214    fn setup_compiler() -> (CachedCompiler, PathBuf) {
215        use std::sync::atomic::{AtomicU64, Ordering};
216        static COUNTER: AtomicU64 = AtomicU64::new(0);
217
218        let id = COUNTER.fetch_add(1, Ordering::SeqCst);
219        let test_dir = env::temp_dir().join(format!("oxur-compiler-test-{}", id));
220
221        // Clean test directory if it exists from a previous run
222        if test_dir.exists() {
223            std::fs::remove_dir_all(&test_dir).expect("Failed to clean test directory");
224        }
225
226        let cache = ArtifactCache::with_directory(&test_dir).expect("Failed to create cache");
227
228        let session_id = SessionId::new(format!("test-{}", id));
229        let session_dir =
230            Arc::new(SessionDir::new(&session_id).expect("Failed to create session dir"));
231
232        let compiler = CachedCompiler::new(cache, session_dir);
233
234        (compiler, test_dir)
235    }
236
237    #[test]
238    fn test_compiler_creation() {
239        let (compiler, _test_dir) = setup_compiler();
240        let (count, _total_size) = compiler.cache().stats();
241        assert_eq!(count, 0);
242    }
243
244    #[test]
245    fn test_compile_simple_library() {
246        let (mut compiler, _test_dir) = setup_compiler();
247
248        let source = r#"
249            #[no_mangle]
250            pub extern "C" fn oxur_eval_test() {
251                // Simple test function
252            }
253        "#;
254
255        let result = compiler.compile("test_simple", source, 0);
256
257        match result {
258            Ok(path) => {
259                assert!(path.exists(), "Compiled library should exist");
260            }
261            Err(e) => {
262                // Compilation might fail in test environment without full rustc setup
263                eprintln!("Note: Compilation failed (expected in some test environments): {}", e);
264            }
265        }
266    }
267
268    #[test]
269    fn test_compile_with_caching() {
270        let (mut compiler, _test_dir) = setup_compiler();
271
272        let source = r#"
273            #[no_mangle]
274            pub extern "C" fn oxur_eval_cache_test() {}
275        "#;
276
277        // First compilation
278        let result1 = compiler.compile("cache_test", source, 0);
279
280        if let Ok(path1) = result1 {
281            // Second compilation with same source should hit cache
282            let result2 = compiler.compile("cache_test", source, 0);
283
284            if let Ok(path2) = result2 {
285                assert_eq!(path1, path2, "Should return same cached path");
286            }
287        }
288    }
289
290    #[test]
291    fn test_compile_invalid_source() {
292        let (mut compiler, _test_dir) = setup_compiler();
293
294        let invalid_source = "this is not valid rust code {{{";
295
296        let result = compiler.compile("invalid", invalid_source, 0);
297
298        assert!(result.is_err(), "Compilation of invalid source should fail");
299
300        if let Err(CompilerError::CompilationFailed(msg)) = result {
301            assert!(!msg.is_empty(), "Should have compilation error message");
302        }
303    }
304
305    #[test]
306    fn test_different_opt_levels() {
307        let (mut compiler, _test_dir) = setup_compiler();
308
309        let source = r#"
310            #[no_mangle]
311            pub extern "C" fn oxur_eval_opt_test() {}
312        "#;
313
314        // Compile with different optimization levels
315        let result_opt0 = compiler.compile("opt0", source, 0);
316        let result_opt2 = compiler.compile("opt2", source, 2);
317
318        // Both should compile (or both fail in test environment)
319        assert_eq!(result_opt0.is_ok(), result_opt2.is_ok(), "Opt levels should have same outcome");
320    }
321}