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}