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