Skip to main content

run/engine/
cpp.rs

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