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