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