ruchy/backend/
compiler.rs

1//! Binary compilation support for Ruchy
2//! 
3//! This module provides functionality to compile Ruchy code to standalone binaries
4//! via Rust compilation toolchain (rustc).
5
6use anyhow::{Context, Result, bail};
7use std::fs;
8use std::path::{Path, PathBuf};
9use std::process::Command;
10use tempfile::TempDir;
11
12use crate::{Parser, Transpiler};
13
14/// Binary compilation options
15#[derive(Debug, Clone)]
16pub struct CompileOptions {
17    /// Output binary path
18    pub output: PathBuf,
19    /// Optimization level (0-3, or 's' for size)
20    pub opt_level: String,
21    /// Strip debug symbols
22    pub strip: bool,
23    /// Static linking
24    pub static_link: bool,
25    /// Target triple (e.g., x86_64-unknown-linux-gnu)
26    pub target: Option<String>,
27    /// Additional rustc flags
28    pub rustc_flags: Vec<String>,
29}
30
31impl Default for CompileOptions {
32    fn default() -> Self {
33        Self {
34            output: PathBuf::from("a.out"),
35            opt_level: "2".to_string(),
36            strip: false,
37            static_link: false,
38            target: None,
39            rustc_flags: Vec::new(),
40        }
41    }
42}
43
44/// Compile a Ruchy source file to a standalone binary
45///
46/// # Examples
47///
48/// ```no_run
49/// use ruchy::backend::{compile_to_binary, CompileOptions};
50/// use std::path::PathBuf;
51///
52/// let options = CompileOptions {
53///     output: PathBuf::from("my_program"),
54///     opt_level: "2".to_string(),
55///     strip: false,
56///     static_link: false,
57///     target: None,
58///     rustc_flags: Vec::new(),
59/// };
60/// 
61/// let result = compile_to_binary(&PathBuf::from("program.ruchy"), &options);
62/// ```
63///
64/// # Errors
65/// 
66/// Returns an error if:
67/// - The source file cannot be read
68/// - The source code fails to parse
69/// - The transpilation fails
70/// - The rustc compilation fails
71pub fn compile_to_binary(source_path: &Path, options: &CompileOptions) -> Result<PathBuf> {
72    // Read source file
73    let source = fs::read_to_string(source_path)
74        .with_context(|| format!("Failed to read source file: {}", source_path.display()))?;
75    
76    compile_source_to_binary(&source, options)
77}
78
79/// Compile Ruchy source code to a standalone binary
80///
81/// # Examples
82///
83/// ```no_run
84/// use ruchy::backend::{compile_source_to_binary, CompileOptions};
85/// use std::path::PathBuf;
86///
87/// let source = r#"
88///     fun main() {
89///         println("Hello, World!");
90///     }
91/// "#;
92///
93/// let options = CompileOptions::default();
94/// let result = compile_source_to_binary(source, &options);
95/// ```
96///
97/// # Errors
98///
99/// Returns an error if:
100/// - The source code fails to parse
101/// - The transpilation fails
102/// - The temporary directory cannot be created
103/// - The rustc compilation fails
104pub fn compile_source_to_binary(source: &str, options: &CompileOptions) -> Result<PathBuf> {
105    // Parse the Ruchy source
106    let mut parser = Parser::new(source);
107    let ast = parser.parse()
108        .context("Failed to parse Ruchy source")?;
109    
110    // Transpile to Rust
111    let transpiler = Transpiler::new();
112    let rust_code = transpiler.transpile_to_program(&ast)
113        .context("Failed to transpile to Rust")?;
114    
115    // Create temporary directory for compilation
116    let temp_dir = TempDir::new()
117        .context("Failed to create temporary directory")?;
118    
119    // Write Rust code to temporary file
120    let rust_file = temp_dir.path().join("main.rs");
121    fs::write(&rust_file, rust_code.to_string())
122        .context("Failed to write Rust code to temporary file")?;
123    
124    // Build rustc command
125    let mut cmd = Command::new("rustc");
126    cmd.arg(&rust_file)
127        .arg("-o")
128        .arg(&options.output);
129    
130    // Add optimization level
131    cmd.arg("-C").arg(format!("opt-level={}", options.opt_level));
132    
133    // Add strip flag if requested
134    if options.strip {
135        cmd.arg("-C").arg("strip=symbols");
136    }
137    
138    // Add static linking if requested
139    if options.static_link {
140        cmd.arg("-C").arg("target-feature=+crt-static");
141    }
142    
143    // Add target if specified
144    if let Some(target) = &options.target {
145        cmd.arg("--target").arg(target);
146    }
147    
148    // Add additional flags
149    for flag in &options.rustc_flags {
150        cmd.arg(flag);
151    }
152    
153    // Execute rustc
154    let output = cmd.output()
155        .context("Failed to execute rustc")?;
156    
157    if !output.status.success() {
158        let stderr = String::from_utf8_lossy(&output.stderr);
159        bail!("Compilation failed:\n{}", stderr);
160    }
161    
162    // Ensure the output file exists
163    if !options.output.exists() {
164        bail!("Expected output file not created: {}", options.output.display());
165    }
166    
167    Ok(options.output.clone())
168}
169
170/// Check if rustc is available
171///
172/// # Examples
173///
174/// ```
175/// use ruchy::backend::compiler::check_rustc_available;
176///
177/// if check_rustc_available().is_ok() {
178///     println!("rustc is available");
179/// }
180/// ```
181///
182/// # Errors
183///
184/// Returns an error if rustc is not installed or cannot be executed
185pub fn check_rustc_available() -> Result<()> {
186    let output = Command::new("rustc")
187        .arg("--version")
188        .output()
189        .context("Failed to execute rustc")?;
190    
191    if !output.status.success() {
192        bail!("rustc is not available. Please install Rust toolchain.");
193    }
194    
195    Ok(())
196}
197
198/// Get rustc version information
199///
200/// # Examples
201///
202/// ```
203/// use ruchy::backend::compiler::get_rustc_version;
204///
205/// if let Ok(version) = get_rustc_version() {
206///     println!("rustc version: {}", version);
207/// }
208/// ```
209///
210/// # Errors
211///
212/// Returns an error if rustc is not available or version cannot be retrieved
213pub fn get_rustc_version() -> Result<String> {
214    let output = Command::new("rustc")
215        .arg("--version")
216        .output()
217        .context("Failed to execute rustc")?;
218    
219    if !output.status.success() {
220        bail!("Failed to get rustc version");
221    }
222    
223    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    
230    #[test]
231    fn test_check_rustc_available() {
232        // This should pass in any environment with Rust installed
233        assert!(check_rustc_available().is_ok());
234    }
235    
236    #[test]
237    fn test_get_rustc_version() {
238        let version = get_rustc_version().unwrap_or_else(|_| "unknown".to_string());
239        assert!(version.contains("rustc"));
240    }
241    
242    #[test]
243    fn test_compile_simple_program() {
244        let source = r#"
245            fun main() {
246                println("Hello from compiled Ruchy!");
247            }
248        "#;
249        
250        let options = CompileOptions {
251            output: PathBuf::from("/tmp/test_ruchy_binary"),
252            ..Default::default()
253        };
254        
255        // This might fail if the transpiler doesn't support the syntax yet
256        // but the infrastructure should work
257        let _ = compile_source_to_binary(source, &options);
258    }
259}