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