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