Coverage Report

Created: 2025-09-08 21:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}