oxur_repl/compiler/
cached.rs1use crate::cache::ArtifactCache;
8use crate::session::SessionDir;
9use std::path::PathBuf;
10use std::process::Command;
11use std::sync::Arc;
12use thiserror::Error;
13
14#[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#[derive(Debug, Clone)]
70pub struct CachedCompiler {
71 cache: ArtifactCache,
73
74 session_dir: Arc<SessionDir>,
76}
77
78impl CachedCompiler {
79 pub fn new(cache: ArtifactCache, session_dir: Arc<SessionDir>) -> Self {
86 Self { cache, session_dir }
87 }
88
89 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 let content_key = self.cache.generate_key(
120 source,
121 &[], opt_level,
123 "default", );
125
126 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 let lib_path = self.compile_to_dylib(cache_key, source, opt_level)?;
135
136 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 fn compile_to_dylib(&self, cache_key: &str, source: &str, opt_level: u8) -> Result<PathBuf> {
147 let source_path = self.session_dir.write_source(format!("{}.rs", cache_key), source)?;
149
150 #[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 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 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 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 return Err(CompilerError::CompilationFailed(stderr.to_string()));
190 }
191 }
192 }
193
194 Ok(lib_path)
195 }
196
197 pub fn cache(&self) -> &ArtifactCache {
199 &self.cache
200 }
201
202 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 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 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 let result1 = compiler.compile("cache_test", source, 0);
279
280 if let Ok(path1) = result1 {
281 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 let result_opt0 = compiler.compile("opt0", source, 0);
316 let result_opt2 = compiler.compile("opt2", source, 2);
317
318 assert_eq!(result_opt0.is_ok(), result_opt2.is_ok(), "Opt levels should have same outcome");
320 }
321}