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 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}