blockly_rust_compiler/
rust_compiler.rs

1// Rust Compiler Integration for Blockly Editor
2// Provides compilation checking and error reporting for generated Rust code
3
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::io::Write;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10/// Compilation result with errors and warnings
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct CompilationResult {
13    pub success: bool,
14    pub errors: Vec<CompilationError>,
15    pub warnings: Vec<CompilationError>,
16    pub stdout: String,
17    pub stderr: String,
18}
19
20/// Individual compilation error or warning
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct CompilationError {
23    pub level: ErrorLevel,
24    pub message: String,
25    pub code: Option<String>,
26    pub line: Option<usize>,
27    pub column: Option<usize>,
28    pub file: Option<String>,
29    pub suggestion: Option<String>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33#[serde(rename_all = "lowercase")]
34pub enum ErrorLevel {
35    Error,
36    Warning,
37    Note,
38    Help,
39}
40
41/// Rust compiler checker
42pub struct RustCompiler {
43    temp_dir: PathBuf,
44}
45
46impl RustCompiler {
47    /// Create a new Rust compiler checker
48    pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
49        let temp_dir = std::env::temp_dir().join("blockly_rust_check");
50        fs::create_dir_all(&temp_dir)?;
51        
52        Ok(Self { temp_dir })
53    }
54
55    /// Check Rust code for compilation errors
56    /// 
57    /// This creates a temporary Rust project and runs `cargo check` to validate the code
58    pub fn check_code(&self, code: &str) -> Result<CompilationResult, Box<dyn std::error::Error>> {
59        // Create a temporary Cargo project
60        let project_dir = self.temp_dir.join(format!("check_{}", uuid::Uuid::new_v4()));
61        fs::create_dir_all(&project_dir)?;
62
63        // Create Cargo.toml
64        let cargo_toml = r#"[package]
65name = "blockly_check"
66version = "0.1.0"
67edition = "2021"
68
69[dependencies]
70"#;
71        fs::write(project_dir.join("Cargo.toml"), cargo_toml)?;
72
73        // Create src directory and main.rs
74        let src_dir = project_dir.join("src");
75        fs::create_dir_all(&src_dir)?;
76        
77        // Wrap code in a main function if it doesn't have one
78        let wrapped_code = if !code.contains("fn main") {
79            format!("fn main() {{\n{}\n}}", code)
80        } else {
81            code.to_string()
82        };
83        
84        fs::write(src_dir.join("main.rs"), wrapped_code)?;
85
86        // Run cargo check with JSON output
87        let output = Command::new("cargo")
88            .arg("check")
89            .arg("--message-format=json")
90            .current_dir(&project_dir)
91            .output()?;
92
93        // Parse the output
94        let result = self.parse_cargo_output(&output.stdout, &output.stderr)?;
95
96        // Clean up temporary directory
97        let _ = fs::remove_dir_all(&project_dir);
98
99        Ok(result)
100    }
101
102    /// Check Rust code with custom dependencies
103    pub fn check_code_with_deps(
104        &self,
105        code: &str,
106        dependencies: &[(&str, &str)],
107    ) -> Result<CompilationResult, Box<dyn std::error::Error>> {
108        // Create a temporary Cargo project
109        let project_dir = self.temp_dir.join(format!("check_{}", uuid::Uuid::new_v4()));
110        fs::create_dir_all(&project_dir)?;
111
112        // Create Cargo.toml with dependencies
113        let mut cargo_toml = String::from(
114            r#"[package]
115name = "blockly_check"
116version = "0.1.0"
117edition = "2021"
118
119[dependencies]
120"#,
121        );
122
123        for (name, version) in dependencies {
124            cargo_toml.push_str(&format!("{} = \"{}\"\n", name, version));
125        }
126
127        fs::write(project_dir.join("Cargo.toml"), cargo_toml)?;
128
129        // Create src directory and main.rs
130        let src_dir = project_dir.join("src");
131        fs::create_dir_all(&src_dir)?;
132        fs::write(src_dir.join("main.rs"), code)?;
133
134        // Run cargo check with JSON output
135        let output = Command::new("cargo")
136            .arg("check")
137            .arg("--message-format=json")
138            .current_dir(&project_dir)
139            .output()?;
140
141        // Parse the output
142        let result = self.parse_cargo_output(&output.stdout, &output.stderr)?;
143
144        // Clean up temporary directory
145        let _ = fs::remove_dir_all(&project_dir);
146
147        Ok(result)
148    }
149
150    /// Quick syntax check without full compilation
151    /// Uses rustc directly for faster feedback
152    pub fn quick_check(&self, code: &str) -> Result<CompilationResult, Box<dyn std::error::Error>> {
153        // Create temporary file
154        let temp_file = self.temp_dir.join(format!("check_{}.rs", uuid::Uuid::new_v4()));
155        fs::write(&temp_file, code)?;
156
157        // Run rustc with JSON output
158        let output = Command::new("rustc")
159            .arg("--crate-type=lib")
160            .arg("--error-format=json")
161            .arg(&temp_file)
162            .arg("-o")
163            .arg("/dev/null") // Don't create output file
164            .output()?;
165
166        // Parse the output
167        let result = self.parse_rustc_output(&output.stdout, &output.stderr)?;
168
169        // Clean up
170        let _ = fs::remove_file(&temp_file);
171
172        Ok(result)
173    }
174
175    /// Parse cargo check JSON output
176    fn parse_cargo_output(
177        &self,
178        stdout: &[u8],
179        stderr: &[u8],
180    ) -> Result<CompilationResult, Box<dyn std::error::Error>> {
181        let stdout_str = String::from_utf8_lossy(stdout);
182        let stderr_str = String::from_utf8_lossy(stderr);
183
184        let mut errors = Vec::new();
185        let mut warnings = Vec::new();
186
187        // Parse JSON messages from cargo
188        for line in stdout_str.lines() {
189            if let Ok(msg) = serde_json::from_str::<serde_json::Value>(line) {
190                if let Some(message) = msg.get("message") {
191                    if let Some(rendered) = message.get("rendered").and_then(|v| v.as_str()) {
192                        let level = message
193                            .get("level")
194                            .and_then(|v| v.as_str())
195                            .unwrap_or("error");
196
197                        let error = CompilationError {
198                            level: match level {
199                                "error" => ErrorLevel::Error,
200                                "warning" => ErrorLevel::Warning,
201                                "note" => ErrorLevel::Note,
202                                "help" => ErrorLevel::Help,
203                                _ => ErrorLevel::Error,
204                            },
205                            message: rendered.to_string(),
206                            code: message
207                                .get("code")
208                                .and_then(|c| c.get("code"))
209                                .and_then(|v| v.as_str())
210                                .map(String::from),
211                            line: message
212                                .get("spans")
213                                .and_then(|s| s.as_array())
214                                .and_then(|arr| arr.first())
215                                .and_then(|span| span.get("line_start"))
216                                .and_then(|v| v.as_u64())
217                                .map(|n| n as usize),
218                            column: message
219                                .get("spans")
220                                .and_then(|s| s.as_array())
221                                .and_then(|arr| arr.first())
222                                .and_then(|span| span.get("column_start"))
223                                .and_then(|v| v.as_u64())
224                                .map(|n| n as usize),
225                            file: message
226                                .get("spans")
227                                .and_then(|s| s.as_array())
228                                .and_then(|arr| arr.first())
229                                .and_then(|span| span.get("file_name"))
230                                .and_then(|v| v.as_str())
231                                .map(String::from),
232                            suggestion: None,
233                        };
234
235                        match error.level {
236                            ErrorLevel::Error => errors.push(error),
237                            ErrorLevel::Warning => warnings.push(error),
238                            _ => {}
239                        }
240                    }
241                }
242            }
243        }
244
245        Ok(CompilationResult {
246            success: errors.is_empty(),
247            errors,
248            warnings,
249            stdout: stdout_str.to_string(),
250            stderr: stderr_str.to_string(),
251        })
252    }
253
254    /// Parse rustc JSON output
255    fn parse_rustc_output(
256        &self,
257        stdout: &[u8],
258        stderr: &[u8],
259    ) -> Result<CompilationResult, Box<dyn std::error::Error>> {
260        // Similar to parse_cargo_output but for rustc
261        self.parse_cargo_output(stdout, stderr)
262    }
263}
264
265impl Default for RustCompiler {
266    fn default() -> Self {
267        Self::new().expect("Failed to create RustCompiler")
268    }
269}
270
271/// Check if Rust toolchain is available
272pub fn is_rust_available() -> bool {
273    Command::new("rustc")
274        .arg("--version")
275        .output()
276        .map(|output| output.status.success())
277        .unwrap_or(false)
278}
279
280/// Check if Cargo is available
281pub fn is_cargo_available() -> bool {
282    Command::new("cargo")
283        .arg("--version")
284        .output()
285        .map(|output| output.status.success())
286        .unwrap_or(false)
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    #[test]
294    fn test_rust_available() {
295        // This test will pass if Rust is installed
296        let available = is_rust_available();
297        println!("Rust available: {}", available);
298    }
299
300    #[test]
301    fn test_valid_code() {
302        if !is_cargo_available() {
303            println!("Skipping test: cargo not available");
304            return;
305        }
306
307        let compiler = RustCompiler::new().unwrap();
308        let code = r#"
309            fn add(a: i32, b: i32) -> i32 {
310                a + b
311            }
312        "#;
313
314        let result = compiler.check_code(code).unwrap();
315        assert!(result.success, "Valid code should compile");
316    }
317
318    #[test]
319    fn test_invalid_code() {
320        if !is_cargo_available() {
321            println!("Skipping test: cargo not available");
322            return;
323        }
324
325        let compiler = RustCompiler::new().unwrap();
326        let code = r#"
327            fn add(a: i32, b: i32) -> i32 {
328                a + // Missing operand
329            }
330        "#;
331
332        let result = compiler.check_code(code).unwrap();
333        assert!(!result.success, "Invalid code should not compile");
334        assert!(!result.errors.is_empty(), "Should have errors");
335    }
336}