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