Skip to main content

every_other_token/
code_executor.rs

1//! Sandboxed code block extraction and simulation.
2//!
3//! Extracts fenced code blocks from Markdown text, simulates execution for
4//! common patterns, and validates basic syntax properties.
5
6use std::collections::HashMap;
7
8// ---------------------------------------------------------------------------
9// Language
10// ---------------------------------------------------------------------------
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum Language {
14    Rust,
15    Python,
16    JavaScript,
17    TypeScript,
18    Go,
19    Bash,
20    Unknown(String),
21}
22
23impl std::fmt::Display for Language {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        match self {
26            Language::Rust => write!(f, "rust"),
27            Language::Python => write!(f, "python"),
28            Language::JavaScript => write!(f, "javascript"),
29            Language::TypeScript => write!(f, "typescript"),
30            Language::Go => write!(f, "go"),
31            Language::Bash => write!(f, "bash"),
32            Language::Unknown(s) => write!(f, "{}", s),
33        }
34    }
35}
36
37// ---------------------------------------------------------------------------
38// CodeBlock
39// ---------------------------------------------------------------------------
40
41#[derive(Debug, Clone)]
42pub struct CodeBlock {
43    pub language: Language,
44    pub code: String,
45    pub start_line: usize,
46    pub end_line: usize,
47}
48
49// ---------------------------------------------------------------------------
50// ExecutionMode
51// ---------------------------------------------------------------------------
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum ExecutionMode {
55    Simulate,
56    Extract,
57    Validate,
58}
59
60// ---------------------------------------------------------------------------
61// SimulationResult
62// ---------------------------------------------------------------------------
63
64#[derive(Debug, Clone)]
65pub struct SimulationResult {
66    pub block: CodeBlock,
67    pub output: String,
68    pub errors: Vec<String>,
69    pub warnings: Vec<String>,
70    pub execution_ms: u64,
71}
72
73// ---------------------------------------------------------------------------
74// CodeExtractor
75// ---------------------------------------------------------------------------
76
77pub struct CodeExtractor;
78
79impl CodeExtractor {
80    /// Extract all fenced code blocks from the given Markdown text.
81    pub fn extract(text: &str) -> Vec<CodeBlock> {
82        let mut blocks = Vec::new();
83        let lines: Vec<&str> = text.lines().collect();
84        let mut i = 0;
85
86        while i < lines.len() {
87            let line = lines[i].trim();
88            if line.starts_with("```") {
89                let tag = line.trim_start_matches('`').trim();
90                let language = Self::detect_language(tag);
91                let start_line = i + 1; // 1-based, line after the opening fence
92                i += 1;
93
94                let mut code_lines = Vec::new();
95                while i < lines.len() {
96                    let inner = lines[i];
97                    if inner.trim() == "```" {
98                        break;
99                    }
100                    code_lines.push(inner);
101                    i += 1;
102                }
103                let end_line = i; // line of closing fence
104
105                blocks.push(CodeBlock {
106                    language,
107                    code: code_lines.join("\n"),
108                    start_line,
109                    end_line,
110                });
111            }
112            i += 1;
113        }
114
115        blocks
116    }
117
118    /// Detect programming language from a fence tag string.
119    pub fn detect_language(tag: &str) -> Language {
120        match tag.to_lowercase().trim() {
121            "rust" | "rs" => Language::Rust,
122            "python" | "py" => Language::Python,
123            "javascript" | "js" => Language::JavaScript,
124            "typescript" | "ts" => Language::TypeScript,
125            "go" | "golang" => Language::Go,
126            "bash" | "sh" | "shell" => Language::Bash,
127            other => Language::Unknown(other.to_string()),
128        }
129    }
130}
131
132// ---------------------------------------------------------------------------
133// CodeSimulator
134// ---------------------------------------------------------------------------
135
136pub struct CodeSimulator {
137    pub mode: ExecutionMode,
138}
139
140impl CodeSimulator {
141    pub fn new(mode: ExecutionMode) -> Self {
142        Self { mode }
143    }
144
145    /// Simulate execution of a code block, producing plausible output.
146    pub fn simulate(&self, block: &CodeBlock) -> SimulationResult {
147        let start = std::time::Instant::now();
148        let mut output = String::new();
149        let mut errors = Vec::new();
150        let warnings = Vec::new();
151
152        match &block.language {
153            Language::Rust => {
154                if block.code.contains("panic!") {
155                    errors.push("thread 'main' panicked".to_string());
156                } else if block.code.contains("println!") {
157                    output = Self::extract_rust_println(&block.code);
158                } else {
159                    output = "Execution simulated".to_string();
160                }
161            }
162            Language::Python => {
163                if block.code.contains("raise") {
164                    errors.push("Traceback (most recent call last): RuntimeError".to_string());
165                } else if block.code.contains("print(") {
166                    output = Self::extract_python_print(&block.code);
167                } else {
168                    output = "Execution simulated".to_string();
169                }
170            }
171            Language::Bash => {
172                if block.code.contains("echo ") {
173                    output = Self::extract_bash_echo(&block.code);
174                } else if block.code.contains("ls") {
175                    output = "[file list simulation]".to_string();
176                } else {
177                    output = "Execution simulated".to_string();
178                }
179            }
180            _ => {
181                output = "Execution simulated".to_string();
182            }
183        }
184
185        SimulationResult {
186            block: block.clone(),
187            output,
188            errors,
189            warnings,
190            execution_ms: start.elapsed().as_millis() as u64,
191        }
192    }
193
194    fn extract_rust_println(code: &str) -> String {
195        // Find println!("...") or println!("{}", ...) style
196        let mut results = Vec::new();
197        for line in code.lines() {
198            let trimmed = line.trim();
199            if let Some(start) = trimmed.find("println!(") {
200                let after = &trimmed[start + 9..];
201                // Extract the string literal
202                if let Some(content) = Self::extract_string_arg(after) {
203                    // Simplistic: just output the literal, stripping format specifiers
204                    results.push(content);
205                }
206            }
207        }
208        results.join("\n")
209    }
210
211    fn extract_python_print(code: &str) -> String {
212        let mut results = Vec::new();
213        for line in code.lines() {
214            let trimmed = line.trim();
215            if let Some(start) = trimmed.find("print(") {
216                let after = &trimmed[start + 6..];
217                if let Some(content) = Self::extract_string_arg(after) {
218                    results.push(content);
219                }
220            }
221        }
222        results.join("\n")
223    }
224
225    fn extract_bash_echo(code: &str) -> String {
226        let mut results = Vec::new();
227        for line in code.lines() {
228            let trimmed = line.trim();
229            if trimmed.starts_with("echo ") {
230                let arg = trimmed[5..].trim().trim_matches('"').trim_matches('\'');
231                results.push(arg.to_string());
232            }
233        }
234        results.join("\n")
235    }
236
237    fn extract_string_arg(s: &str) -> Option<String> {
238        // Find first " or ' and extract until the matching close
239        let s = s.trim();
240        if s.starts_with('"') {
241            let inner = &s[1..];
242            if let Some(end) = inner.find('"') {
243                return Some(inner[..end].to_string());
244            }
245        } else if s.starts_with('\'') {
246            let inner = &s[1..];
247            if let Some(end) = inner.find('\'') {
248                return Some(inner[..end].to_string());
249            }
250        }
251        None
252    }
253
254    /// Validate basic syntax properties of a code block.
255    pub fn validate_syntax(block: &CodeBlock) -> Vec<String> {
256        let mut errors = Vec::new();
257        let code = &block.code;
258
259        // Check balanced braces
260        let open_braces = code.chars().filter(|&c| c == '{').count();
261        let close_braces = code.chars().filter(|&c| c == '}').count();
262        if open_braces != close_braces {
263            errors.push(format!(
264                "Unbalanced braces: {} opening, {} closing",
265                open_braces, close_braces
266            ));
267        }
268
269        // Check balanced brackets
270        let open_brackets = code.chars().filter(|&c| c == '[').count();
271        let close_brackets = code.chars().filter(|&c| c == ']').count();
272        if open_brackets != close_brackets {
273            errors.push(format!(
274                "Unbalanced brackets: {} opening, {} closing",
275                open_brackets, close_brackets
276            ));
277        }
278
279        // Check balanced parens
280        let open_parens = code.chars().filter(|&c| c == '(').count();
281        let close_parens = code.chars().filter(|&c| c == ')').count();
282        if open_parens != close_parens {
283            errors.push(format!(
284                "Unbalanced parentheses: {} opening, {} closing",
285                open_parens, close_parens
286            ));
287        }
288
289        // Check for unclosed strings (odd number of unescaped double quotes)
290        // Simple heuristic: count non-escaped "
291        let quote_count = code.chars().filter(|&c| c == '"').count();
292        if quote_count % 2 != 0 {
293            errors.push("Possible unclosed string: odd number of double-quote characters".to_string());
294        }
295
296        errors
297    }
298}
299
300// ---------------------------------------------------------------------------
301// CodeReport
302// ---------------------------------------------------------------------------
303
304#[derive(Debug, Clone)]
305pub struct CodeReport {
306    pub total_blocks: usize,
307    pub by_language: HashMap<String, usize>,
308    pub total_lines: usize,
309    pub syntax_errors: usize,
310}
311
312/// Analyze a slice of code blocks and produce a summary report.
313pub fn analyze_blocks(blocks: &[CodeBlock]) -> CodeReport {
314    let mut by_language: HashMap<String, usize> = HashMap::new();
315    let mut total_lines = 0usize;
316    let mut syntax_errors = 0usize;
317
318    for block in blocks {
319        let lang_key = block.language.to_string();
320        *by_language.entry(lang_key).or_insert(0) += 1;
321
322        let lines = block.code.lines().count();
323        total_lines += lines;
324
325        let errs = CodeSimulator::validate_syntax(block);
326        syntax_errors += errs.len();
327    }
328
329    CodeReport {
330        total_blocks: blocks.len(),
331        by_language,
332        total_lines,
333        syntax_errors,
334    }
335}
336
337// ---------------------------------------------------------------------------
338// Tests
339// ---------------------------------------------------------------------------
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344
345    const SAMPLE_MD: &str = r#"
346Some text here.
347
348```rust
349fn main() {
350    println!("Hello, world!");
351}
352```
353
354Another paragraph.
355
356```python
357def greet():
358    print("Hello from Python")
359```
360
361```bash
362echo "Hello from bash"
363ls
364```
365"#;
366
367    #[test]
368    fn test_extract_finds_code_blocks() {
369        let blocks = CodeExtractor::extract(SAMPLE_MD);
370        assert_eq!(blocks.len(), 3);
371    }
372
373    #[test]
374    fn test_language_detection() {
375        assert_eq!(CodeExtractor::detect_language("rust"), Language::Rust);
376        assert_eq!(CodeExtractor::detect_language("py"), Language::Python);
377        assert_eq!(CodeExtractor::detect_language("js"), Language::JavaScript);
378        assert_eq!(CodeExtractor::detect_language("ts"), Language::TypeScript);
379        assert_eq!(CodeExtractor::detect_language("go"), Language::Go);
380        assert_eq!(CodeExtractor::detect_language("sh"), Language::Bash);
381        assert_eq!(
382            CodeExtractor::detect_language("haskell"),
383            Language::Unknown("haskell".to_string())
384        );
385    }
386
387    #[test]
388    fn test_simulate_rust_println() {
389        let block = CodeBlock {
390            language: Language::Rust,
391            code: r#"fn main() { println!("Hello, world!"); }"#.to_string(),
392            start_line: 1,
393            end_line: 1,
394        };
395        let sim = CodeSimulator::new(ExecutionMode::Simulate);
396        let result = sim.simulate(&block);
397        assert!(result.output.contains("Hello, world!"));
398        assert!(result.errors.is_empty());
399    }
400
401    #[test]
402    fn test_simulate_python_print() {
403        let block = CodeBlock {
404            language: Language::Python,
405            code: "print(\"Hello from Python\")".to_string(),
406            start_line: 1,
407            end_line: 1,
408        };
409        let sim = CodeSimulator::new(ExecutionMode::Simulate);
410        let result = sim.simulate(&block);
411        assert!(result.output.contains("Hello from Python"));
412    }
413
414    #[test]
415    fn test_simulate_bash_echo() {
416        let block = CodeBlock {
417            language: Language::Bash,
418            code: "echo \"Hello from bash\"".to_string(),
419            start_line: 1,
420            end_line: 1,
421        };
422        let sim = CodeSimulator::new(ExecutionMode::Simulate);
423        let result = sim.simulate(&block);
424        assert!(result.output.contains("Hello from bash"));
425    }
426
427    #[test]
428    fn test_validate_catches_unbalanced_braces() {
429        let block = CodeBlock {
430            language: Language::Rust,
431            code: "fn main() { let x = 1;".to_string(),
432            start_line: 1,
433            end_line: 1,
434        };
435        let errors = CodeSimulator::validate_syntax(&block);
436        assert!(!errors.is_empty());
437        assert!(errors[0].contains("brace"));
438    }
439
440    #[test]
441    fn test_validate_balanced_code() {
442        let block = CodeBlock {
443            language: Language::Rust,
444            code: "fn main() { let x = 1; }".to_string(),
445            start_line: 1,
446            end_line: 1,
447        };
448        let errors = CodeSimulator::validate_syntax(&block);
449        assert!(errors.is_empty());
450    }
451
452    #[test]
453    fn test_analyze_blocks() {
454        let blocks = CodeExtractor::extract(SAMPLE_MD);
455        let report = analyze_blocks(&blocks);
456        assert_eq!(report.total_blocks, 3);
457        assert!(report.by_language.contains_key("rust"));
458        assert!(report.by_language.contains_key("python"));
459        assert!(report.by_language.contains_key("bash"));
460    }
461
462    #[test]
463    fn test_rust_panic_produces_error() {
464        let block = CodeBlock {
465            language: Language::Rust,
466            code: r#"fn main() { panic!("something went wrong"); }"#.to_string(),
467            start_line: 1,
468            end_line: 1,
469        };
470        let sim = CodeSimulator::new(ExecutionMode::Simulate);
471        let result = sim.simulate(&block);
472        assert!(!result.errors.is_empty());
473    }
474}