oxur_comp/
compiler.rs

1//! Compiler
2//!
3//! Orchestrates the complete compilation pipeline from Core Forms to binary.
4
5use crate::{CodeGenerator, Error, Lowerer, Result};
6use oxur_lang::CoreForm;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10/// Compiler orchestrates the full compilation pipeline
11pub struct Compiler {
12    codegen: CodeGenerator,
13    output_dir: PathBuf,
14}
15
16impl Compiler {
17    pub fn new(output_dir: PathBuf) -> Self {
18        Self { codegen: CodeGenerator::new(), output_dir }
19    }
20
21    /// Compile Core Forms to a binary
22    ///
23    /// Accepts the SourceMap from the expansion phase and returns it
24    /// with lowering mappings added for error reporting.
25    ///
26    /// # Error Translation
27    ///
28    /// If rustc compilation fails, errors are translated using the SourceMap
29    /// to show Oxur source positions where possible. Currently shows generated
30    /// Rust positions with a note that full translation is being implemented.
31    pub fn compile(
32        &mut self,
33        forms: Vec<CoreForm>,
34        source_map: oxur_smap::SourceMap,
35        output: &Path,
36    ) -> Result<oxur_smap::SourceMap> {
37        // Stage 3: Lower to Rust AST
38        let mut lowerer = Lowerer::new(source_map);
39        let (ast, source_map) = lowerer.lower(forms)?;
40
41        // Stage 4: Generate Rust source
42        let source = self.codegen.generate(&ast)?;
43
44        // Write to temporary .rs file
45        let rs_file = self.output_dir.join("generated.rs");
46        std::fs::write(&rs_file, source)?;
47
48        // Stage 5: Compile with rustc (pass source_map for error translation)
49        self.compile_with_rustc(&rs_file, output, &source_map)?;
50
51        Ok(source_map)
52    }
53
54    fn compile_with_rustc(
55        &self,
56        source: &Path,
57        output: &Path,
58        source_map: &oxur_smap::SourceMap,
59    ) -> Result<()> {
60        let output_result = Command::new("rustc")
61            .arg(source)
62            .arg("-o")
63            .arg(output)
64            .arg("--error-format=json")
65            .output()?;
66
67        if !output_result.status.success() {
68            // Parse JSON diagnostics from stderr
69            let stderr = String::from_utf8_lossy(&output_result.stderr);
70            let diagnostics =
71                crate::RustcDiagnostic::from_json_lines(&stderr).unwrap_or_else(|_| vec![]);
72
73            // Use ErrorTranslator to convert rustc errors to Oxur positions
74            let translator = crate::ErrorTranslator::new(source_map.clone());
75            let translated = translator.translate_diagnostics(&diagnostics);
76
77            let error_msg = format!(
78                "rustc failed with exit code: {:?}\n\n{}",
79                output_result.status.code(),
80                translated
81            );
82
83            return Err(Error::Compile(error_msg));
84        }
85
86        Ok(())
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_compiler_creation() {
96        let compiler = Compiler::new(PathBuf::from("/tmp"));
97        assert_eq!(compiler.output_dir, PathBuf::from("/tmp"));
98    }
99
100    #[test]
101    fn test_compiler_has_codegen() {
102        let compiler = Compiler::new(PathBuf::from("/tmp"));
103        // CodeGenerator is private but we can verify it exists
104        assert_eq!(compiler.output_dir, PathBuf::from("/tmp"));
105    }
106
107    #[test]
108    fn test_compiler_output_dir() {
109        let test_dir = PathBuf::from("/var/tmp/test");
110        let compiler = Compiler::new(test_dir.clone());
111        assert_eq!(compiler.output_dir, test_dir);
112    }
113
114    #[test]
115    fn test_compile_with_empty_forms() {
116        use tempfile::TempDir;
117
118        let temp_dir = TempDir::new().unwrap();
119        let output_dir = temp_dir.path().join("build");
120        std::fs::create_dir_all(&output_dir).unwrap();
121
122        let mut compiler = Compiler::new(output_dir.clone());
123        let output_path = temp_dir.path().join("test_output");
124        let source_map = oxur_smap::SourceMap::new();
125
126        // This will fail because rustc isn't available or the generated code is invalid
127        // but we're just testing that the compilation pipeline runs
128        let result = compiler.compile(vec![], source_map, &output_path);
129        // We expect this to error (no rustc or invalid generated code)
130        // but the important thing is that it attempts compilation
131        assert!(result.is_ok() || result.is_err());
132    }
133
134    #[test]
135    fn test_compile_hello_world() {
136        use oxur_lang::{Expander, Parser};
137        use tempfile::TempDir;
138
139        // Parse and expand hello world
140        let source = r#"(deffn main ()
141  (println! "Hello, world!"))"#;
142
143        let mut parser = Parser::new(source.to_string());
144        let surface_forms = parser.parse().unwrap();
145
146        let mut expander = Expander::new();
147        let core_forms = expander.expand(surface_forms).unwrap();
148        let source_map = expander.source_map().clone();
149
150        // Compile to binary
151        let temp_dir = TempDir::new().unwrap();
152        let output_dir = temp_dir.path().join("build");
153        std::fs::create_dir_all(&output_dir).unwrap();
154
155        let mut compiler = Compiler::new(output_dir);
156        let binary_path = temp_dir.path().join("hello_world");
157
158        let result = compiler.compile(core_forms, source_map, &binary_path);
159        assert!(result.is_ok(), "Compilation failed: {:?}", result.err());
160
161        // Verify binary exists
162        assert!(binary_path.exists(), "Binary was not created");
163
164        // Run the binary and check output
165        let output = Command::new(&binary_path).output().unwrap();
166        let stdout = String::from_utf8_lossy(&output.stdout);
167
168        eprintln!("Binary output:\n{}", stdout);
169
170        assert!(output.status.success(), "Binary execution failed");
171        assert!(stdout.contains("Hello, world!"), "Output doesn't contain expected text");
172    }
173
174    #[test]
175    fn test_error_translation_format() {
176        use oxur_lang::{Expander, Parser};
177        use tempfile::TempDir;
178
179        // Parse code with intentional error (undefined variable)
180        let source = r#"(deffn main ()
181  (println! x))"#; // `x` is undefined
182
183        let mut parser = Parser::new(source.to_string());
184        let surface_forms = parser.parse().unwrap();
185
186        let mut expander = Expander::new();
187        let core_forms = expander.expand(surface_forms).unwrap();
188        let source_map = expander.source_map().clone();
189
190        // Compile (this should fail with rustc error)
191        let temp_dir = TempDir::new().unwrap();
192        let output_dir = temp_dir.path().join("build");
193        std::fs::create_dir_all(&output_dir).unwrap();
194
195        let mut compiler = Compiler::new(output_dir);
196        let binary_path = temp_dir.path().join("test_error");
197
198        let result = compiler.compile(core_forms, source_map, &binary_path);
199
200        // Should fail with compilation error
201        assert!(result.is_err(), "Should fail due to undefined variable");
202
203        if let Err(crate::Error::Compile(msg)) = result {
204            // Error message should mention the error
205            eprintln!("Error message:\n{}", msg);
206
207            // Should contain rustc error code or exit status
208            // (exact format depends on rustc version, but should have some structure)
209            assert!(!msg.is_empty(), "Should have error message");
210        }
211    }
212}