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