Skip to main content

run/engine/
cpp.rs

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            // Rehydrate persistent cache even when incremental workspace is already up-to-date.
217            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        // Try cache for inline/stdin payloads
277        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}