/home/noah/src/ruchy/src/backend/compiler.rs
Line | Count | Source |
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 | | |
6 | | use anyhow::{Context, Result, bail}; |
7 | | use std::fs; |
8 | | use std::path::{Path, PathBuf}; |
9 | | use std::process::Command; |
10 | | use tempfile::TempDir; |
11 | | |
12 | | use crate::{Parser, Transpiler}; |
13 | | |
14 | | /// Binary compilation options |
15 | | #[derive(Debug, Clone)] |
16 | | pub 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 | | |
31 | | impl Default for CompileOptions { |
32 | 1 | fn default() -> Self { |
33 | 1 | Self { |
34 | 1 | output: PathBuf::from("a.out"), |
35 | 1 | opt_level: "2".to_string(), |
36 | 1 | strip: false, |
37 | 1 | static_link: false, |
38 | 1 | target: None, |
39 | 1 | rustc_flags: Vec::new(), |
40 | 1 | } |
41 | 1 | } |
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 |
71 | 0 | pub fn compile_to_binary(source_path: &Path, options: &CompileOptions) -> Result<PathBuf> { |
72 | | // Read source file |
73 | 0 | let source = fs::read_to_string(source_path) |
74 | 0 | .with_context(|| format!("Failed to read source file: {}", source_path.display()))?; |
75 | | |
76 | 0 | compile_source_to_binary(&source, options) |
77 | 0 | } |
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 working directory cannot be created |
103 | | /// - The rustc compilation fails |
104 | 1 | pub fn compile_source_to_binary(source: &str, options: &CompileOptions) -> Result<PathBuf> { |
105 | | // Parse the Ruchy source |
106 | 1 | let mut parser = Parser::new(source); |
107 | 1 | let ast = parser.parse() |
108 | 1 | .context("Failed to parse Ruchy source")?0 ; |
109 | | |
110 | | // Transpile to Rust |
111 | 1 | eprintln!("DEBUG: About to call transpile_to_program"); |
112 | 1 | let mut transpiler = Transpiler::new(); |
113 | 1 | let rust_code = transpiler.transpile_to_program(&ast) |
114 | 1 | .context("Failed to transpile to Rust")?0 ; |
115 | 1 | eprintln!("DEBUG: transpile_to_program completed"); |
116 | | |
117 | | // Create working directory for compilation |
118 | 1 | let temp_dir = TempDir::new() |
119 | 1 | .context("Failed to create temporary directory")?0 ; |
120 | | |
121 | | // Write Rust code to working file |
122 | 1 | let rust_file = temp_dir.path().join("main.rs"); |
123 | 1 | let rust_code_str = rust_code.to_string(); |
124 | | |
125 | | // Debug: Also write to /tmp/debug_rust_output.rs for inspection |
126 | 1 | fs::write("/tmp/debug_rust_output.rs", &rust_code_str) |
127 | 1 | .context("Failed to write debug Rust code")?0 ; |
128 | | |
129 | 1 | fs::write(&rust_file, rust_code_str) |
130 | 1 | .context("Failed to write Rust code to temporary file")?0 ; |
131 | | |
132 | | // Build rustc command |
133 | 1 | let mut cmd = Command::new("rustc"); |
134 | 1 | cmd.arg(&rust_file) |
135 | 1 | .arg("-o") |
136 | 1 | .arg(&options.output); |
137 | | |
138 | | // Add optimization level |
139 | 1 | cmd.arg("-C").arg(format!("opt-level={}", options.opt_level)); |
140 | | |
141 | | // Add strip flag if requested |
142 | 1 | if options.strip { |
143 | 0 | cmd.arg("-C").arg("strip=symbols"); |
144 | 1 | } |
145 | | |
146 | | // Add static linking if requested |
147 | 1 | if options.static_link { |
148 | 0 | cmd.arg("-C").arg("target-feature=+crt-static"); |
149 | 1 | } |
150 | | |
151 | | // Add target if specified |
152 | 1 | if let Some(target0 ) = &options.target { |
153 | 0 | cmd.arg("--target").arg(target); |
154 | 1 | } |
155 | | |
156 | | // Add additional flags |
157 | 1 | for flag0 in &options.rustc_flags { |
158 | 0 | cmd.arg(flag); |
159 | 0 | } |
160 | | |
161 | | // Execute rustc |
162 | 1 | let output = cmd.output() |
163 | 1 | .context("Failed to execute rustc")?0 ; |
164 | | |
165 | 1 | if !output.status.success() { |
166 | 0 | let stderr = String::from_utf8_lossy(&output.stderr); |
167 | 0 | bail!("Compilation failed:\n{}", stderr); |
168 | 1 | } |
169 | | |
170 | | // Ensure the output file exists |
171 | 1 | if !options.output.exists() { |
172 | 0 | bail!("Expected output file not created: {}", options.output.display()); |
173 | 1 | } |
174 | | |
175 | 1 | Ok(options.output.clone()) |
176 | 1 | } |
177 | | |
178 | | /// Check if rustc is available |
179 | | /// |
180 | | /// # Examples |
181 | | /// |
182 | | /// ``` |
183 | | /// use ruchy::backend::compiler::check_rustc_available; |
184 | | /// |
185 | | /// if check_rustc_available().is_ok() { |
186 | | /// println!("rustc is available"); |
187 | | /// } |
188 | | /// ``` |
189 | | /// |
190 | | /// # Errors |
191 | | /// |
192 | | /// Returns an error if rustc is not installed or cannot be executed |
193 | 1 | pub fn check_rustc_available() -> Result<()> { |
194 | 1 | let output = Command::new("rustc") |
195 | 1 | .arg("--version") |
196 | 1 | .output() |
197 | 1 | .context("Failed to execute rustc")?0 ; |
198 | | |
199 | 1 | if !output.status.success() { |
200 | 0 | bail!("rustc is not available. Please install Rust toolchain."); |
201 | 1 | } |
202 | | |
203 | 1 | Ok(()) |
204 | 1 | } |
205 | | |
206 | | /// Get rustc version information |
207 | | /// |
208 | | /// # Examples |
209 | | /// |
210 | | /// ``` |
211 | | /// use ruchy::backend::compiler::get_rustc_version; |
212 | | /// |
213 | | /// if let Ok(version) = get_rustc_version() { |
214 | | /// println!("rustc version: {}", version); |
215 | | /// } |
216 | | /// ``` |
217 | | /// |
218 | | /// # Errors |
219 | | /// |
220 | | /// Returns an error if rustc is not available or version cannot be retrieved |
221 | 1 | pub fn get_rustc_version() -> Result<String> { |
222 | 1 | let output = Command::new("rustc") |
223 | 1 | .arg("--version") |
224 | 1 | .output() |
225 | 1 | .context("Failed to execute rustc")?0 ; |
226 | | |
227 | 1 | if !output.status.success() { |
228 | 0 | bail!("Failed to get rustc version"); |
229 | 1 | } |
230 | | |
231 | 1 | Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) |
232 | 1 | } |
233 | | |
234 | | #[cfg(test)] |
235 | | mod tests { |
236 | | use super::*; |
237 | | |
238 | | #[test] |
239 | 1 | fn test_check_rustc_available() { |
240 | | // This should pass in any environment with Rust installed |
241 | 1 | assert!(check_rustc_available().is_ok()); |
242 | 1 | } |
243 | | |
244 | | #[test] |
245 | 1 | fn test_get_rustc_version() { |
246 | 1 | let version = get_rustc_version().unwrap_or_else(|_| "unknown"0 .to_string0 ()); |
247 | 1 | assert!(version.contains("rustc")); |
248 | 1 | } |
249 | | |
250 | | #[test] |
251 | 1 | fn test_compile_simple_program() { |
252 | 1 | let source = r#" |
253 | 1 | fun main() { |
254 | 1 | println("Hello from compiled Ruchy!"); |
255 | 1 | } |
256 | 1 | "#; |
257 | | |
258 | 1 | let options = CompileOptions { |
259 | 1 | output: PathBuf::from("/tmp/test_ruchy_binary"), |
260 | 1 | ..Default::default() |
261 | 1 | }; |
262 | | |
263 | | // This might fail if the transpiler doesn't support the syntax yet |
264 | | // but the infrastructure should work |
265 | 1 | let _ = compile_source_to_binary(source, &options); |
266 | 1 | } |
267 | | } |