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}