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