Skip to main content

run/engine/
c.rs

1use std::collections::BTreeSet;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::process::{Command, Stdio};
5use std::time::Instant;
6
7use anyhow::{Context, Result};
8use tempfile::{Builder, TempDir};
9
10use super::{
11    ExecutionOutcome, ExecutionPayload, LanguageEngine, LanguageSession, cache_lookup, cache_store,
12    compiler_command, hash_source, perf_record, run_version_command, try_cached_execution,
13};
14
15pub struct CEngine {
16    compiler: Option<PathBuf>,
17}
18
19impl Default for CEngine {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl CEngine {
26    pub fn new() -> Self {
27        Self {
28            compiler: resolve_c_compiler(),
29        }
30    }
31
32    fn ensure_compiler(&self) -> Result<&Path> {
33        self.compiler.as_deref().ok_or_else(|| {
34            anyhow::anyhow!(
35                "C support requires a C compiler such as `cc`, `clang`, or `gcc`. Install one and ensure it is on your PATH."
36            )
37        })
38    }
39
40    fn write_source(&self, code: &str, dir: &Path) -> Result<PathBuf> {
41        let source_path = dir.join("main.c");
42        let prepared = prepare_inline_source(code);
43        std::fs::write(&source_path, prepared).with_context(|| {
44            format!(
45                "failed to write temporary C source to {}",
46                source_path.display()
47            )
48        })?;
49        Ok(source_path)
50    }
51
52    fn copy_source(&self, original: &Path, dir: &Path) -> Result<PathBuf> {
53        let target = dir.join("main.c");
54        std::fs::copy(original, &target).with_context(|| {
55            format!(
56                "failed to copy C source from {} to {}",
57                original.display(),
58                target.display()
59            )
60        })?;
61        Ok(target)
62    }
63
64    fn compile(&self, source: &Path, output: &Path) -> Result<std::process::Output> {
65        let compiler = self.ensure_compiler()?;
66        let mut cmd = compiler_command(compiler);
67        cmd.arg(source)
68            .arg("-std=c11")
69            .arg("-O0")
70            .arg("-w")
71            .arg("-o")
72            .arg(output)
73            .stdout(Stdio::piped())
74            .stderr(Stdio::piped());
75        cmd.output().with_context(|| {
76            format!(
77                "failed to invoke {} to compile {}",
78                compiler.display(),
79                source.display()
80            )
81        })
82    }
83
84    fn run_binary(&self, binary: &Path, args: &[String]) -> Result<std::process::Output> {
85        let mut cmd = Command::new(binary);
86        cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped());
87        cmd.stdin(Stdio::inherit());
88        cmd.output()
89            .with_context(|| format!("failed to execute compiled binary {}", binary.display()))
90    }
91
92    fn binary_path(dir: &Path) -> PathBuf {
93        let mut path = dir.join("run_c_binary");
94        let suffix = std::env::consts::EXE_SUFFIX;
95        if !suffix.is_empty() {
96            if let Some(stripped) = suffix.strip_prefix('.') {
97                path.set_extension(stripped);
98            } else {
99                path = PathBuf::from(format!("{}{}", path.display(), suffix));
100            }
101        }
102        path
103    }
104
105    fn execute_file_incremental(&self, source: &Path, args: &[String]) -> Result<ExecutionOutcome> {
106        let start = Instant::now();
107        let source_text = fs::read_to_string(source).unwrap_or_default();
108        let source_hash = hash_source(&source_text);
109
110        let compiler = self.ensure_compiler()?;
111        let source_key = source
112            .canonicalize()
113            .unwrap_or_else(|_| source.to_path_buf());
114        let workspace =
115            crate::cache::workspace("c-file", hash_source(&source_key.to_string_lossy()))?;
116        fs::create_dir_all(&workspace).with_context(|| {
117            format!(
118                "failed to create C incremental workspace {}",
119                workspace.display()
120            )
121        })?;
122        let obj = workspace.join("main.o");
123        let dep = workspace.join("main.d");
124        let bin = workspace.join("run_c_incremental_binary");
125
126        let needs_compile = c_needs_recompile(source, &obj, &dep);
127        if !needs_compile && bin.exists() {
128            perf_record("c", "file.workspace_hit");
129            cache_store("c-file", source_hash, &bin);
130            let run_output = self.run_binary(&bin, args)?;
131            return Ok(ExecutionOutcome {
132                language: self.id().to_string(),
133                exit_code: run_output.status.code(),
134                stdout: String::from_utf8_lossy(&run_output.stdout).into_owned(),
135                stderr: String::from_utf8_lossy(&run_output.stderr).into_owned(),
136                duration: start.elapsed(),
137            });
138        }
139
140        if let Some(cached_bin) = cache_lookup("c-file", source_hash) {
141            perf_record("c", "file.cache_hit");
142            let _ = fs::copy(&cached_bin, &bin);
143            let run_output = self.run_binary(&bin, args)?;
144            return Ok(ExecutionOutcome {
145                language: self.id().to_string(),
146                exit_code: run_output.status.code(),
147                stdout: String::from_utf8_lossy(&run_output.stdout).into_owned(),
148                stderr: String::from_utf8_lossy(&run_output.stderr).into_owned(),
149                duration: start.elapsed(),
150            });
151        }
152        perf_record("c", "file.cache_miss");
153
154        if needs_compile {
155            perf_record("c", "file.compile");
156            let mut compile = compiler_command(compiler);
157            compile
158                .arg(source)
159                .arg("-std=c11")
160                .arg("-O0")
161                .arg("-w")
162                .arg("-c")
163                .arg("-MMD")
164                .arg("-MF")
165                .arg(&dep)
166                .arg("-o")
167                .arg(&obj)
168                .stdout(Stdio::piped())
169                .stderr(Stdio::piped());
170            let compile_out = compile.output().with_context(|| {
171                format!(
172                    "failed to invoke {} for incremental C compile",
173                    compiler.display()
174                )
175            })?;
176            if !compile_out.status.success() {
177                perf_record("c", "file.compile_fail");
178                return Ok(ExecutionOutcome {
179                    language: self.id().to_string(),
180                    exit_code: compile_out.status.code(),
181                    stdout: String::from_utf8_lossy(&compile_out.stdout).into_owned(),
182                    stderr: String::from_utf8_lossy(&compile_out.stderr).into_owned(),
183                    duration: start.elapsed(),
184                });
185            }
186
187            let mut link = compiler_command(compiler);
188            perf_record("c", "file.link");
189            link.arg(&obj)
190                .arg("-o")
191                .arg(&bin)
192                .stdout(Stdio::piped())
193                .stderr(Stdio::piped());
194            let link_out = link.output().with_context(|| {
195                format!(
196                    "failed to invoke {} for incremental C link",
197                    compiler.display()
198                )
199            })?;
200            if !link_out.status.success() {
201                perf_record("c", "file.link_fail");
202                return Ok(ExecutionOutcome {
203                    language: self.id().to_string(),
204                    exit_code: link_out.status.code(),
205                    stdout: String::from_utf8_lossy(&link_out.stdout).into_owned(),
206                    stderr: String::from_utf8_lossy(&link_out.stderr).into_owned(),
207                    duration: start.elapsed(),
208                });
209            }
210            cache_store("c-file", source_hash, &bin);
211        } else {
212            // Rehydrate persistent cache even when incremental workspace is already up-to-date.
213            perf_record("c", "file.rehydrate_cache");
214            cache_store("c-file", source_hash, &bin);
215        }
216
217        let run_output = self.run_binary(&bin, args)?;
218        Ok(ExecutionOutcome {
219            language: self.id().to_string(),
220            exit_code: run_output.status.code(),
221            stdout: String::from_utf8_lossy(&run_output.stdout).into_owned(),
222            stderr: String::from_utf8_lossy(&run_output.stderr).into_owned(),
223            duration: start.elapsed(),
224        })
225    }
226}
227
228impl LanguageEngine for CEngine {
229    fn id(&self) -> &'static str {
230        "c"
231    }
232
233    fn display_name(&self) -> &'static str {
234        "C"
235    }
236
237    fn aliases(&self) -> &[&'static str] {
238        &["ansi-c"]
239    }
240
241    fn supports_sessions(&self) -> bool {
242        true
243    }
244
245    fn validate(&self) -> Result<()> {
246        let compiler = self.ensure_compiler()?;
247        let mut cmd = Command::new(compiler);
248        cmd.arg("--version")
249            .stdout(Stdio::null())
250            .stderr(Stdio::null());
251        cmd.status()
252            .with_context(|| format!("failed to invoke {}", compiler.display()))?
253            .success()
254            .then_some(())
255            .ok_or_else(|| anyhow::anyhow!("{} is not executable", compiler.display()))
256    }
257
258    fn toolchain_version(&self) -> Result<Option<String>> {
259        let compiler = self.ensure_compiler()?;
260        let mut cmd = Command::new(compiler);
261        cmd.arg("--version");
262        let context = format!("{}", compiler.display());
263        run_version_command(cmd, &context)
264    }
265
266    fn execute(&self, payload: &ExecutionPayload) -> Result<ExecutionOutcome> {
267        // Try cache for inline/stdin payloads
268        let args = payload.args();
269        if let ExecutionPayload::File { path, .. } = payload {
270            return self.execute_file_incremental(path, args);
271        }
272
273        if let Some(code) = match payload {
274            ExecutionPayload::Inline { code, .. } | ExecutionPayload::Stdin { code, .. } => {
275                Some(code.as_str())
276            }
277            _ => None,
278        } {
279            let prepared = prepare_inline_source(code);
280            let src_hash = hash_source(&prepared);
281            if let Some(output) = try_cached_execution("c", src_hash) {
282                perf_record("c", "inline.cache_hit");
283                let start = Instant::now();
284                return Ok(ExecutionOutcome {
285                    language: self.id().to_string(),
286                    exit_code: output.status.code(),
287                    stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
288                    stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
289                    duration: start.elapsed(),
290                });
291            }
292            perf_record("c", "inline.cache_miss");
293        }
294
295        let temp_dir = Builder::new()
296            .prefix("run-c")
297            .tempdir()
298            .context("failed to create temporary directory for c build")?;
299        let dir_path = temp_dir.path();
300
301        let (source_path, cache_key) = match payload {
302            ExecutionPayload::Inline { code, .. } | ExecutionPayload::Stdin { code, .. } => {
303                let prepared = prepare_inline_source(code);
304                let h = hash_source(&prepared);
305                (self.write_source(code, dir_path)?, Some(h))
306            }
307            ExecutionPayload::File { path, .. } => (self.copy_source(path, dir_path)?, None),
308        };
309
310        let binary_path = Self::binary_path(dir_path);
311        let start = Instant::now();
312
313        let compile_output = self.compile(&source_path, &binary_path)?;
314        if !compile_output.status.success() {
315            return Ok(ExecutionOutcome {
316                language: self.id().to_string(),
317                exit_code: compile_output.status.code(),
318                stdout: String::from_utf8_lossy(&compile_output.stdout).into_owned(),
319                stderr: String::from_utf8_lossy(&compile_output.stderr).into_owned(),
320                duration: start.elapsed(),
321            });
322        }
323
324        if let Some(h) = cache_key {
325            cache_store("c", h, &binary_path);
326        }
327
328        let run_output = self.run_binary(&binary_path, args)?;
329        Ok(ExecutionOutcome {
330            language: self.id().to_string(),
331            exit_code: run_output.status.code(),
332            stdout: String::from_utf8_lossy(&run_output.stdout).into_owned(),
333            stderr: String::from_utf8_lossy(&run_output.stderr).into_owned(),
334            duration: start.elapsed(),
335        })
336    }
337
338    fn start_session(&self) -> Result<Box<dyn LanguageSession>> {
339        let compiler = self.ensure_compiler()?.to_path_buf();
340        let session = CSession::new(compiler)?;
341        Ok(Box::new(session))
342    }
343}
344
345const SESSION_MAIN_FILE: &str = "main.c";
346
347const PRINT_HELPERS: &str = concat!(
348    "static void __print_int(long long value) { printf(\"%lld\\n\", value); }\n",
349    "static void __print_uint(unsigned long long value) { printf(\"%llu\\n\", value); }\n",
350    "static void __print_double(double value) { printf(\"%0.17g\\n\", value); }\n",
351    "static void __print_cstr(const char *value) { if (!value) { printf(\"(null)\\n\"); } else { printf(\"%s\\n\", value); } }\n",
352    "static void __print_char(int value) { printf(\"%d\\n\", value); }\n",
353    "static void __print_pointer(const void *value) { printf(\"%p\\n\", value); }\n",
354    "#define __print(value) _Generic((value), \\\n",
355    "    char: __print_char, \\\n",
356    "    signed char: __print_int, \\\n",
357    "    short: __print_int, \\\n",
358    "    int: __print_int, \\\n",
359    "    long: __print_int, \\\n",
360    "    long long: __print_int, \\\n",
361    "    unsigned char: __print_uint, \\\n",
362    "    unsigned short: __print_uint, \\\n",
363    "    unsigned int: __print_uint, \\\n",
364    "    unsigned long: __print_uint, \\\n",
365    "    unsigned long long: __print_uint, \\\n",
366    "    float: __print_double, \\\n",
367    "    double: __print_double, \\\n",
368    "    long double: __print_double, \\\n",
369    "    char *: __print_cstr, \\\n",
370    "    const char *: __print_cstr, \\\n",
371    "    default: __print_pointer \\\n",
372    ")(value)\n\n",
373);
374
375struct CSession {
376    compiler: PathBuf,
377    workspace: TempDir,
378    includes: BTreeSet<String>,
379    items: Vec<String>,
380    statements: Vec<String>,
381    last_stdout: String,
382    last_stderr: String,
383}
384
385enum CSnippetKind {
386    Include(Option<String>),
387    Item,
388    Statement,
389}
390
391impl CSession {
392    fn new(compiler: PathBuf) -> Result<Self> {
393        let workspace = TempDir::new().context("failed to create C session workspace")?;
394        let session = Self {
395            compiler,
396            workspace,
397            includes: Self::default_includes(),
398            items: Vec::new(),
399            statements: Vec::new(),
400            last_stdout: String::new(),
401            last_stderr: String::new(),
402        };
403        session.persist_source()?;
404        Ok(session)
405    }
406
407    fn default_includes() -> BTreeSet<String> {
408        let mut includes = BTreeSet::new();
409        includes.insert("#include <stdio.h>".to_string());
410        includes.insert("#include <inttypes.h>".to_string());
411        includes
412    }
413
414    fn language_id(&self) -> &str {
415        "c"
416    }
417
418    fn source_path(&self) -> PathBuf {
419        self.workspace.path().join(SESSION_MAIN_FILE)
420    }
421
422    fn binary_path(&self) -> PathBuf {
423        CEngine::binary_path(self.workspace.path())
424    }
425
426    fn persist_source(&self) -> Result<()> {
427        let source = self.render_source();
428        fs::write(self.source_path(), source)
429            .with_context(|| "failed to write C session source".to_string())
430    }
431
432    fn render_source(&self) -> String {
433        let mut source = String::new();
434
435        for include in &self.includes {
436            source.push_str(include);
437            if !include.ends_with('\n') {
438                source.push('\n');
439            }
440        }
441
442        source.push('\n');
443        source.push_str(PRINT_HELPERS);
444
445        for item in &self.items {
446            source.push_str(item);
447            if !item.ends_with('\n') {
448                source.push('\n');
449            }
450            source.push('\n');
451        }
452
453        source.push_str("int main(void) {\n");
454        if self.statements.is_empty() {
455            source.push_str("    // session body\n");
456        } else {
457            for snippet in &self.statements {
458                for line in snippet.lines() {
459                    source.push_str("    ");
460                    source.push_str(line);
461                    source.push('\n');
462                }
463            }
464        }
465        source.push_str("    return 0;\n}\n");
466
467        source
468    }
469
470    fn compile(&self) -> Result<std::process::Output> {
471        let source = self.source_path();
472        let binary = self.binary_path();
473        self.compile_with_paths(&source, &binary)
474    }
475
476    fn compile_with_paths(&self, source: &Path, binary: &Path) -> Result<std::process::Output> {
477        let mut cmd = compiler_command(&self.compiler);
478        cmd.arg(source)
479            .arg("-std=c11")
480            .arg("-O0")
481            .arg("-w")
482            .arg("-o")
483            .arg(binary)
484            .stdout(Stdio::piped())
485            .stderr(Stdio::piped());
486        cmd.output().with_context(|| {
487            format!(
488                "failed to invoke {} to compile {}",
489                self.compiler.display(),
490                source.display()
491            )
492        })
493    }
494
495    fn run_binary(&self) -> Result<std::process::Output> {
496        let binary = self.binary_path();
497        self.run_binary_path(&binary)
498    }
499
500    fn run_binary_path(&self, binary: &Path) -> Result<std::process::Output> {
501        let mut cmd = Command::new(binary);
502        cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
503        cmd.output()
504            .with_context(|| format!("failed to execute compiled binary {}", binary.display()))
505    }
506
507    fn run_standalone_program(&self, code: &str) -> Result<ExecutionOutcome> {
508        let start = Instant::now();
509        let source_path = self.workspace.path().join("standalone.c");
510        let binary_path = self.workspace.path().join("standalone_c_binary");
511
512        let mut source = String::new();
513
514        for include in &self.includes {
515            source.push_str(include);
516            if !include.ends_with('\n') {
517                source.push('\n');
518            }
519        }
520
521        source.push('\n');
522        source.push_str(PRINT_HELPERS);
523
524        for item in &self.items {
525            source.push_str(item);
526            if !item.ends_with('\n') {
527                source.push('\n');
528            }
529            source.push('\n');
530        }
531
532        source.push_str(code);
533        if !code.ends_with('\n') {
534            source.push('\n');
535        }
536
537        fs::write(&source_path, source)
538            .with_context(|| "failed to write C standalone source".to_string())?;
539
540        let compile_output = self.compile_with_paths(&source_path, &binary_path)?;
541        if !compile_output.status.success() {
542            return Ok(ExecutionOutcome {
543                language: self.language_id().to_string(),
544                exit_code: compile_output.status.code(),
545                stdout: Self::normalize_output(&compile_output.stdout),
546                stderr: Self::normalize_output(&compile_output.stderr),
547                duration: start.elapsed(),
548            });
549        }
550
551        let run_output = self.run_binary_path(&binary_path)?;
552        Ok(ExecutionOutcome {
553            language: self.language_id().to_string(),
554            exit_code: run_output.status.code(),
555            stdout: Self::normalize_output(&run_output.stdout),
556            stderr: Self::normalize_output(&run_output.stderr),
557            duration: start.elapsed(),
558        })
559    }
560
561    fn add_include(&mut self, line: &str) -> CSnippetKind {
562        let added = self.includes.insert(line.to_string());
563        if added {
564            CSnippetKind::Include(Some(line.to_string()))
565        } else {
566            CSnippetKind::Include(None)
567        }
568    }
569
570    fn add_item(&mut self, code: &str) -> CSnippetKind {
571        let mut snippet = code.to_string();
572        if !snippet.ends_with('\n') {
573            snippet.push('\n');
574        }
575        self.items.push(snippet);
576        CSnippetKind::Item
577    }
578
579    fn add_statement(&mut self, code: &str) -> CSnippetKind {
580        let mut snippet = code.to_string();
581        if !snippet.ends_with('\n') {
582            snippet.push('\n');
583        }
584        self.statements.push(snippet);
585        CSnippetKind::Statement
586    }
587
588    fn add_expression(&mut self, code: &str) -> CSnippetKind {
589        let wrapped = wrap_expression(code);
590        self.statements.push(wrapped);
591        CSnippetKind::Statement
592    }
593
594    fn reset_state(&mut self) -> Result<()> {
595        self.includes = Self::default_includes();
596        self.items.clear();
597        self.statements.clear();
598        self.last_stdout.clear();
599        self.last_stderr.clear();
600        self.persist_source()
601    }
602
603    fn rollback(&mut self, kind: CSnippetKind) -> Result<()> {
604        match kind {
605            CSnippetKind::Include(Some(line)) => {
606                self.includes.remove(&line);
607            }
608            CSnippetKind::Include(None) => {}
609            CSnippetKind::Item => {
610                self.items.pop();
611            }
612            CSnippetKind::Statement => {
613                self.statements.pop();
614            }
615        }
616        self.persist_source()
617    }
618
619    fn normalize_output(bytes: &[u8]) -> String {
620        String::from_utf8_lossy(bytes)
621            .replace("\r\n", "\n")
622            .replace('\r', "")
623    }
624
625    fn diff_outputs(previous: &str, current: &str) -> String {
626        if let Some(suffix) = current.strip_prefix(previous) {
627            suffix.to_string()
628        } else {
629            current.to_string()
630        }
631    }
632
633    fn run_insertion(&mut self, kind: CSnippetKind) -> Result<(ExecutionOutcome, bool)> {
634        if matches!(kind, CSnippetKind::Include(None)) {
635            return Ok((
636                ExecutionOutcome {
637                    language: self.language_id().to_string(),
638                    exit_code: None,
639                    stdout: String::new(),
640                    stderr: String::new(),
641                    duration: Default::default(),
642                },
643                true,
644            ));
645        }
646
647        self.persist_source()?;
648        let start = Instant::now();
649        let compile_output = self.compile()?;
650
651        if !compile_output.status.success() {
652            let duration = start.elapsed();
653            self.rollback(kind)?;
654            let outcome = ExecutionOutcome {
655                language: self.language_id().to_string(),
656                exit_code: compile_output.status.code(),
657                stdout: Self::normalize_output(&compile_output.stdout),
658                stderr: Self::normalize_output(&compile_output.stderr),
659                duration,
660            };
661            return Ok((outcome, false));
662        }
663
664        let run_output = self.run_binary()?;
665        let duration = start.elapsed();
666        let stdout_full = Self::normalize_output(&run_output.stdout);
667        let stderr_full = Self::normalize_output(&run_output.stderr);
668
669        let stdout = Self::diff_outputs(&self.last_stdout, &stdout_full);
670        let stderr = Self::diff_outputs(&self.last_stderr, &stderr_full);
671
672        if run_output.status.success() {
673            self.last_stdout = stdout_full;
674            self.last_stderr = stderr_full;
675            let outcome = ExecutionOutcome {
676                language: self.language_id().to_string(),
677                exit_code: run_output.status.code(),
678                stdout,
679                stderr,
680                duration,
681            };
682            return Ok((outcome, true));
683        }
684
685        self.rollback(kind)?;
686        let outcome = ExecutionOutcome {
687            language: self.language_id().to_string(),
688            exit_code: run_output.status.code(),
689            stdout,
690            stderr,
691            duration,
692        };
693        Ok((outcome, false))
694    }
695
696    fn run_include(&mut self, line: &str) -> Result<(ExecutionOutcome, bool)> {
697        let kind = self.add_include(line);
698        self.run_insertion(kind)
699    }
700
701    fn run_item(&mut self, code: &str) -> Result<(ExecutionOutcome, bool)> {
702        let kind = self.add_item(code);
703        self.run_insertion(kind)
704    }
705
706    fn run_statement(&mut self, code: &str) -> Result<(ExecutionOutcome, bool)> {
707        let kind = self.add_statement(code);
708        self.run_insertion(kind)
709    }
710
711    fn run_expression(&mut self, code: &str) -> Result<(ExecutionOutcome, bool)> {
712        let kind = self.add_expression(code);
713        self.run_insertion(kind)
714    }
715}
716
717impl LanguageSession for CSession {
718    fn language_id(&self) -> &str {
719        CSession::language_id(self)
720    }
721
722    fn eval(&mut self, code: &str) -> Result<ExecutionOutcome> {
723        let trimmed = code.trim();
724        if trimmed.is_empty() {
725            return Ok(ExecutionOutcome {
726                language: self.language_id().to_string(),
727                exit_code: None,
728                stdout: String::new(),
729                stderr: String::new(),
730                duration: Instant::now().elapsed(),
731            });
732        }
733
734        if trimmed.eq_ignore_ascii_case(":reset") {
735            self.reset_state()?;
736            return Ok(ExecutionOutcome {
737                language: self.language_id().to_string(),
738                exit_code: None,
739                stdout: String::new(),
740                stderr: String::new(),
741                duration: Default::default(),
742            });
743        }
744
745        if trimmed.eq_ignore_ascii_case(":help") {
746            return Ok(ExecutionOutcome {
747                language: self.language_id().to_string(),
748                exit_code: None,
749                stdout:
750                    "C commands:\n  :reset - clear session state\n  :help  - show this message\n"
751                        .to_string(),
752                stderr: String::new(),
753                duration: Default::default(),
754            });
755        }
756
757        if contains_main_definition(code) {
758            return self.run_standalone_program(code);
759        }
760
761        if let Some(include) = parse_include(trimmed) {
762            let (outcome, _) = self.run_include(&include)?;
763            return Ok(outcome);
764        }
765
766        if is_item_snippet(trimmed) {
767            let (outcome, _) = self.run_item(code)?;
768            return Ok(outcome);
769        }
770
771        if should_treat_as_expression(trimmed) {
772            let (outcome, success) = self.run_expression(trimmed)?;
773            if success {
774                return Ok(outcome);
775            }
776        }
777
778        let (outcome, _) = self.run_statement(code)?;
779        Ok(outcome)
780    }
781
782    fn shutdown(&mut self) -> Result<()> {
783        Ok(())
784    }
785}
786
787fn contains_main_definition(code: &str) -> bool {
788    let bytes = code.as_bytes();
789    let len = bytes.len();
790    let mut i = 0;
791    let mut in_line_comment = false;
792    let mut in_block_comment = false;
793    let mut in_string = false;
794    let mut string_delim = b'"';
795    let mut in_char = false;
796
797    while i < len {
798        let b = bytes[i];
799
800        if in_line_comment {
801            if b == b'\n' {
802                in_line_comment = false;
803            }
804            i += 1;
805            continue;
806        }
807
808        if in_block_comment {
809            if b == b'*' && i + 1 < len && bytes[i + 1] == b'/' {
810                in_block_comment = false;
811                i += 2;
812                continue;
813            }
814            i += 1;
815            continue;
816        }
817
818        if in_string {
819            if b == b'\\' {
820                i = (i + 2).min(len);
821                continue;
822            }
823            if b == string_delim {
824                in_string = false;
825            }
826            i += 1;
827            continue;
828        }
829
830        if in_char {
831            if b == b'\\' {
832                i = (i + 2).min(len);
833                continue;
834            }
835            if b == b'\'' {
836                in_char = false;
837            }
838            i += 1;
839            continue;
840        }
841
842        match b {
843            b'/' if i + 1 < len && bytes[i + 1] == b'/' => {
844                in_line_comment = true;
845                i += 2;
846                continue;
847            }
848            b'/' if i + 1 < len && bytes[i + 1] == b'*' => {
849                in_block_comment = true;
850                i += 2;
851                continue;
852            }
853            b'"' | b'\'' => {
854                if b == b'"' {
855                    in_string = true;
856                    string_delim = b;
857                } else {
858                    in_char = true;
859                }
860                i += 1;
861                continue;
862            }
863            b'm' if i + 4 <= len && &bytes[i..i + 4] == b"main" => {
864                if i > 0 {
865                    let prev = bytes[i - 1];
866                    if prev.is_ascii_alphanumeric() || prev == b'_' {
867                        i += 1;
868                        continue;
869                    }
870                }
871
872                let after_name = i + 4;
873                if after_name < len {
874                    let next = bytes[after_name];
875                    if next.is_ascii_alphanumeric() || next == b'_' {
876                        i += 1;
877                        continue;
878                    }
879                }
880
881                let mut j = after_name;
882                while j < len && bytes[j].is_ascii_whitespace() {
883                    j += 1;
884                }
885                if j >= len || bytes[j] != b'(' {
886                    i += 1;
887                    continue;
888                }
889
890                let mut depth = 1usize;
891                let mut k = j + 1;
892                let mut inner_line_comment = false;
893                let mut inner_block_comment = false;
894                let mut inner_string = false;
895                let mut inner_char = false;
896
897                while k < len {
898                    let ch = bytes[k];
899
900                    if inner_line_comment {
901                        if ch == b'\n' {
902                            inner_line_comment = false;
903                        }
904                        k += 1;
905                        continue;
906                    }
907
908                    if inner_block_comment {
909                        if ch == b'*' && k + 1 < len && bytes[k + 1] == b'/' {
910                            inner_block_comment = false;
911                            k += 2;
912                            continue;
913                        }
914                        k += 1;
915                        continue;
916                    }
917
918                    if inner_string {
919                        if ch == b'\\' {
920                            k = (k + 2).min(len);
921                            continue;
922                        }
923                        if ch == b'"' {
924                            inner_string = false;
925                        }
926                        k += 1;
927                        continue;
928                    }
929
930                    if inner_char {
931                        if ch == b'\\' {
932                            k = (k + 2).min(len);
933                            continue;
934                        }
935                        if ch == b'\'' {
936                            inner_char = false;
937                        }
938                        k += 1;
939                        continue;
940                    }
941
942                    match ch {
943                        b'/' if k + 1 < len && bytes[k + 1] == b'/' => {
944                            inner_line_comment = true;
945                            k += 2;
946                            continue;
947                        }
948                        b'/' if k + 1 < len && bytes[k + 1] == b'*' => {
949                            inner_block_comment = true;
950                            k += 2;
951                            continue;
952                        }
953                        b'"' => {
954                            inner_string = true;
955                            k += 1;
956                            continue;
957                        }
958                        b'\'' => {
959                            inner_char = true;
960                            k += 1;
961                            continue;
962                        }
963                        b'(' => {
964                            depth += 1;
965                        }
966                        b')' => {
967                            depth -= 1;
968                            k += 1;
969                            if depth == 0 {
970                                break;
971                            } else {
972                                continue;
973                            }
974                        }
975                        _ => {}
976                    }
977
978                    k += 1;
979                }
980
981                if depth != 0 {
982                    i += 1;
983                    continue;
984                }
985
986                let mut after = k;
987                loop {
988                    while after < len && bytes[after].is_ascii_whitespace() {
989                        after += 1;
990                    }
991                    if after + 1 < len && bytes[after] == b'/' && bytes[after + 1] == b'/' {
992                        after += 2;
993                        while after < len && bytes[after] != b'\n' {
994                            after += 1;
995                        }
996                        continue;
997                    }
998                    if after + 1 < len && bytes[after] == b'/' && bytes[after + 1] == b'*' {
999                        after += 2;
1000                        while after + 1 < len {
1001                            if bytes[after] == b'*' && bytes[after + 1] == b'/' {
1002                                after += 2;
1003                                break;
1004                            }
1005                            after += 1;
1006                        }
1007                        continue;
1008                    }
1009                    break;
1010                }
1011
1012                if after < len && bytes[after] == b'{' {
1013                    return true;
1014                }
1015            }
1016            _ => {}
1017        }
1018
1019        i += 1;
1020    }
1021
1022    false
1023}
1024
1025fn parse_include(code: &str) -> Option<String> {
1026    let trimmed = code.trim_start();
1027    if !trimmed.starts_with("#include") {
1028        return None;
1029    }
1030    let line = trimmed.lines().next()?.trim().to_string();
1031    if line.is_empty() { None } else { Some(line) }
1032}
1033
1034fn is_item_snippet(code: &str) -> bool {
1035    let trimmed = code.trim_start();
1036    if trimmed.starts_with("#include") {
1037        return false;
1038    }
1039
1040    if trimmed.starts_with('#') {
1041        return true;
1042    }
1043
1044    const KEYWORDS: [&str; 8] = [
1045        "typedef", "struct", "union", "enum", "extern", "static", "const", "volatile",
1046    ];
1047    if KEYWORDS.iter().any(|kw| trimmed.starts_with(kw)) {
1048        return true;
1049    }
1050
1051    if let Some(open_brace) = trimmed.find('{') {
1052        let before_brace = trimmed[..open_brace].trim();
1053        if before_brace.ends_with(';') {
1054            return false;
1055        }
1056
1057        const CONTROL_KEYWORDS: [&str; 5] = ["if", "for", "while", "switch", "do"];
1058        if CONTROL_KEYWORDS
1059            .iter()
1060            .any(|kw| before_brace.starts_with(kw))
1061        {
1062            return false;
1063        }
1064
1065        if before_brace.contains('(') {
1066            return true;
1067        }
1068    }
1069
1070    if trimmed.ends_with(';') {
1071        if trimmed.contains('(') && trimmed.contains(')') {
1072            let before_paren = trimmed.split('(').next().unwrap_or_default();
1073            if before_paren.split_whitespace().count() >= 2 {
1074                return true;
1075            }
1076        }
1077
1078        let first_token = trimmed.split_whitespace().next().unwrap_or_default();
1079        const TYPE_PREFIXES: [&str; 12] = [
1080            "auto", "register", "signed", "unsigned", "short", "long", "int", "char", "float",
1081            "double", "_Bool", "void",
1082        ];
1083        if TYPE_PREFIXES.contains(&first_token) {
1084            return true;
1085        }
1086    }
1087
1088    false
1089}
1090
1091fn should_treat_as_expression(code: &str) -> bool {
1092    let trimmed = code.trim();
1093    if trimmed.is_empty() {
1094        return false;
1095    }
1096    if trimmed.contains('\n') {
1097        return false;
1098    }
1099    if trimmed.ends_with(';') {
1100        return false;
1101    }
1102    const DISALLOWED_PREFIXES: [&str; 21] = [
1103        "#", "typedef", "struct", "union", "enum", "extern", "static", "const", "volatile", "void",
1104        "auto", "signed", "register", "unsigned", "short", "long", "int", "char", "float",
1105        "double", "_Bool",
1106    ];
1107    if DISALLOWED_PREFIXES.iter().any(|kw| trimmed.starts_with(kw)) {
1108        return false;
1109    }
1110    true
1111}
1112
1113fn wrap_expression(code: &str) -> String {
1114    format!("__print({});\n", code)
1115}
1116
1117fn prepare_inline_source(code: &str) -> String {
1118    if !needs_wrapper(code) {
1119        return code.to_string();
1120    }
1121
1122    if code.trim().is_empty() {
1123        return "#include <stdio.h>\n\nint main(void)\n{\n    return 0;\n}\n".to_string();
1124    }
1125
1126    let body = indent_snippet(code);
1127    format!("#include <stdio.h>\n\nint main(void)\n{{\n{body}    return 0;\n}}\n",)
1128}
1129
1130fn needs_wrapper(code: &str) -> bool {
1131    let trimmed = code.trim();
1132    if trimmed.is_empty() {
1133        return true;
1134    }
1135
1136    !(trimmed.contains("#include") || trimmed.contains("main("))
1137}
1138
1139fn indent_snippet(snippet: &str) -> String {
1140    let mut result = String::new();
1141    for line in snippet.lines() {
1142        if line.trim().is_empty() {
1143            result.push('\n');
1144        } else {
1145            result.push_str("    ");
1146            result.push_str(line);
1147            result.push('\n');
1148        }
1149    }
1150
1151    result
1152}
1153
1154fn resolve_c_compiler() -> Option<PathBuf> {
1155    ["cc", "clang", "gcc"]
1156        .into_iter()
1157        .find_map(|candidate| which::which(candidate).ok())
1158}
1159
1160fn c_needs_recompile(source: &Path, object: &Path, depfile: &Path) -> bool {
1161    if !object.exists() {
1162        return true;
1163    }
1164    let obj_time = match object.metadata().and_then(|m| m.modified()) {
1165        Ok(t) => t,
1166        Err(_) => return true,
1167    };
1168    let src_time = match source.metadata().and_then(|m| m.modified()) {
1169        Ok(t) => t,
1170        Err(_) => return true,
1171    };
1172    if src_time > obj_time {
1173        return true;
1174    }
1175    if !depfile.exists() {
1176        return true;
1177    }
1178    let dep_text = match fs::read_to_string(depfile) {
1179        Ok(t) => t.replace("\\\n", " "),
1180        Err(_) => return true,
1181    };
1182    for token in dep_text.split_whitespace().skip(1) {
1183        let path = token.trim_end_matches(':');
1184        if path.is_empty() {
1185            continue;
1186        }
1187        let p = Path::new(path);
1188        if let Ok(t) = p.metadata().and_then(|m| m.modified())
1189            && t > obj_time
1190        {
1191            return true;
1192        }
1193    }
1194    false
1195}