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_store, hash_source,
11    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 = Command::new(compiler);
65        cmd.arg(source)
66            .arg("-std=c++17")
67            .arg("-O0")
68            .arg("-Wall")
69            .arg("-Wextra")
70            .arg("-o")
71            .arg(output)
72            .stdout(Stdio::piped())
73            .stderr(Stdio::piped());
74        cmd.output().with_context(|| {
75            format!(
76                "failed to invoke {} to compile {}",
77                compiler.display(),
78                source.display()
79            )
80        })
81    }
82
83    fn run_binary(&self, binary: &Path, args: &[String]) -> Result<std::process::Output> {
84        let mut cmd = Command::new(binary);
85        cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped());
86        cmd.stdin(Stdio::inherit());
87        cmd.output()
88            .with_context(|| format!("failed to execute compiled binary {}", binary.display()))
89    }
90
91    fn binary_path(dir: &Path) -> PathBuf {
92        let mut path = dir.join("run_cpp_binary");
93        let suffix = std::env::consts::EXE_SUFFIX;
94        if !suffix.is_empty() {
95            if let Some(stripped) = suffix.strip_prefix('.') {
96                path.set_extension(stripped);
97            } else {
98                path = PathBuf::from(format!("{}{}", path.display(), suffix));
99            }
100        }
101        path
102    }
103}
104
105impl LanguageEngine for CppEngine {
106    fn id(&self) -> &'static str {
107        "cpp"
108    }
109
110    fn display_name(&self) -> &'static str {
111        "C++"
112    }
113
114    fn aliases(&self) -> &[&'static str] {
115        &["c++"]
116    }
117
118    fn supports_sessions(&self) -> bool {
119        self.compiler.is_some()
120    }
121
122    fn validate(&self) -> Result<()> {
123        let compiler = self.ensure_compiler()?;
124        let mut cmd = Command::new(compiler);
125        cmd.arg("--version")
126            .stdout(Stdio::null())
127            .stderr(Stdio::null());
128        cmd.status()
129            .with_context(|| format!("failed to invoke {}", compiler.display()))?
130            .success()
131            .then_some(())
132            .ok_or_else(|| anyhow::anyhow!("{} is not executable", compiler.display()))
133    }
134
135    fn toolchain_version(&self) -> Result<Option<String>> {
136        let compiler = self.ensure_compiler()?;
137        let mut cmd = Command::new(compiler);
138        cmd.arg("--version");
139        let context = format!("{}", compiler.display());
140        run_version_command(cmd, &context)
141    }
142
143    fn execute(&self, payload: &ExecutionPayload) -> Result<ExecutionOutcome> {
144        let args = payload.args();
145
146        // Try cache for inline/stdin payloads
147        if let Some(code) = match payload {
148            ExecutionPayload::Inline { code, .. } | ExecutionPayload::Stdin { code, .. } => {
149                Some(code.as_str())
150            }
151            _ => None,
152        } {
153            let src_hash = hash_source(code);
154            if let Some(output) = try_cached_execution(src_hash) {
155                let start = Instant::now();
156                return Ok(ExecutionOutcome {
157                    language: self.id().to_string(),
158                    exit_code: output.status.code(),
159                    stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
160                    stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
161                    duration: start.elapsed(),
162                });
163            }
164        }
165
166        let temp_dir = Builder::new()
167            .prefix("run-cpp")
168            .tempdir()
169            .context("failed to create temporary directory for cpp build")?;
170        let dir_path = temp_dir.path();
171
172        let (source_path, cache_key) = match payload {
173            ExecutionPayload::Inline { code, .. } | ExecutionPayload::Stdin { code, .. } => {
174                let h = hash_source(code);
175                (self.write_source(code, dir_path)?, Some(h))
176            }
177            ExecutionPayload::File { path, .. } => (self.copy_source(path, dir_path)?, None),
178        };
179
180        let binary_path = Self::binary_path(dir_path);
181        let start = Instant::now();
182
183        let compile_output = self.compile(&source_path, &binary_path)?;
184        if !compile_output.status.success() {
185            return Ok(ExecutionOutcome {
186                language: self.id().to_string(),
187                exit_code: compile_output.status.code(),
188                stdout: String::from_utf8_lossy(&compile_output.stdout).into_owned(),
189                stderr: String::from_utf8_lossy(&compile_output.stderr).into_owned(),
190                duration: start.elapsed(),
191            });
192        }
193
194        if let Some(h) = cache_key {
195            cache_store(h, &binary_path);
196        }
197
198        let run_output = self.run_binary(&binary_path, args)?;
199        Ok(ExecutionOutcome {
200            language: self.id().to_string(),
201            exit_code: run_output.status.code(),
202            stdout: String::from_utf8_lossy(&run_output.stdout).into_owned(),
203            stderr: String::from_utf8_lossy(&run_output.stderr).into_owned(),
204            duration: start.elapsed(),
205        })
206    }
207
208    fn start_session(&self) -> Result<Box<dyn LanguageSession>> {
209        let compiler = self.ensure_compiler().map(Path::to_path_buf)?;
210
211        let temp_dir = Builder::new()
212            .prefix("run-cpp-repl")
213            .tempdir()
214            .context("failed to create temporary directory for cpp repl")?;
215        let dir_path = temp_dir.path();
216        let source_path = dir_path.join("main.cpp");
217        let binary_path = Self::binary_path(dir_path);
218
219        Ok(Box::new(CppSession {
220            compiler,
221            _temp_dir: temp_dir,
222            source_path,
223            binary_path,
224            definitions: Vec::new(),
225            statements: Vec::new(),
226            previous_stdout: String::new(),
227            previous_stderr: String::new(),
228        }))
229    }
230}
231
232fn resolve_cpp_compiler() -> Option<PathBuf> {
233    ["c++", "clang++", "g++"]
234        .into_iter()
235        .find_map(|candidate| which::which(candidate).ok())
236}
237
238const SESSION_PREAMBLE: &str = concat!(
239    "#include <iostream>\n",
240    "#include <iomanip>\n",
241    "#include <string>\n",
242    "#include <vector>\n",
243    "#include <map>\n",
244    "#include <set>\n",
245    "#include <unordered_map>\n",
246    "#include <unordered_set>\n",
247    "#include <deque>\n",
248    "#include <list>\n",
249    "#include <queue>\n",
250    "#include <stack>\n",
251    "#include <memory>\n",
252    "#include <functional>\n",
253    "#include <algorithm>\n",
254    "#include <numeric>\n",
255    "#include <cmath>\n\n",
256    "using namespace std;\n\n",
257);
258
259struct CppSession {
260    compiler: PathBuf,
261    _temp_dir: TempDir,
262    source_path: PathBuf,
263    binary_path: PathBuf,
264    definitions: Vec<String>,
265    statements: Vec<String>,
266    previous_stdout: String,
267    previous_stderr: String,
268}
269
270impl CppSession {
271    fn render_prelude(&self) -> String {
272        let mut source = String::from(SESSION_PREAMBLE);
273        for def in &self.definitions {
274            source.push_str(def);
275            if !def.ends_with('\n') {
276                source.push('\n');
277            }
278            source.push('\n');
279        }
280        source
281    }
282
283    fn render_source(&self) -> String {
284        let mut source = self.render_prelude();
285        source.push_str("int main()\n{\n    ios::sync_with_stdio(false);\n    cin.tie(nullptr);\n    cout.setf(std::ios::boolalpha);\n");
286        for stmt in &self.statements {
287            for line in stmt.lines() {
288                source.push_str("    ");
289                source.push_str(line);
290                source.push('\n');
291            }
292            if !stmt.ends_with('\n') {
293                source.push('\n');
294            }
295        }
296        source.push_str("    return 0;\n}\n");
297        source
298    }
299
300    fn write_source(&self, contents: &str) -> Result<()> {
301        fs::write(&self.source_path, contents).with_context(|| {
302            format!(
303                "failed to write generated C++ REPL source to {}",
304                self.source_path.display()
305            )
306        })
307    }
308
309    fn compile_and_run(&mut self) -> Result<(std::process::Output, Duration)> {
310        let start = Instant::now();
311        let source = self.render_source();
312        self.write_source(&source)?;
313        let compile_output =
314            invoke_cpp_compiler(&self.compiler, &self.source_path, &self.binary_path)?;
315        if !compile_output.status.success() {
316            let duration = start.elapsed();
317            return Ok((compile_output, duration));
318        }
319        let execution_output = run_cpp_binary(&self.binary_path)?;
320        let duration = start.elapsed();
321        Ok((execution_output, duration))
322    }
323
324    fn run_standalone_program(&mut self, code: &str) -> Result<ExecutionOutcome> {
325        let start = Instant::now();
326        let mut source = self.render_prelude();
327        if !source.ends_with('\n') {
328            source.push('\n');
329        }
330        source.push_str(code);
331        if !code.ends_with('\n') {
332            source.push('\n');
333        }
334
335        let standalone_path = self
336            .source_path
337            .parent()
338            .unwrap_or_else(|| Path::new("."))
339            .join("standalone.cpp");
340        fs::write(&standalone_path, &source)
341            .with_context(|| "failed to write standalone C++ source".to_string())?;
342
343        let compile_output =
344            invoke_cpp_compiler(&self.compiler, &standalone_path, &self.binary_path)?;
345        if !compile_output.status.success() {
346            return Ok(ExecutionOutcome {
347                language: "cpp".to_string(),
348                exit_code: compile_output.status.code(),
349                stdout: normalize_output(&compile_output.stdout),
350                stderr: normalize_output(&compile_output.stderr),
351                duration: start.elapsed(),
352            });
353        }
354
355        let run_output = run_cpp_binary(&self.binary_path)?;
356        Ok(ExecutionOutcome {
357            language: "cpp".to_string(),
358            exit_code: run_output.status.code(),
359            stdout: normalize_output(&run_output.stdout),
360            stderr: normalize_output(&run_output.stderr),
361            duration: start.elapsed(),
362        })
363    }
364
365    fn reset_state(&mut self) -> Result<()> {
366        self.definitions.clear();
367        self.statements.clear();
368        self.previous_stdout.clear();
369        self.previous_stderr.clear();
370        let source = self.render_source();
371        self.write_source(&source)
372    }
373
374    fn diff_outputs(
375        &mut self,
376        output: &std::process::Output,
377        duration: Duration,
378    ) -> ExecutionOutcome {
379        let stdout_full = normalize_output(&output.stdout);
380        let stderr_full = normalize_output(&output.stderr);
381
382        let stdout_delta = diff_output(&self.previous_stdout, &stdout_full);
383        let stderr_delta = diff_output(&self.previous_stderr, &stderr_full);
384
385        if output.status.success() {
386            self.previous_stdout = stdout_full;
387            self.previous_stderr = stderr_full;
388        }
389
390        ExecutionOutcome {
391            language: "cpp".to_string(),
392            exit_code: output.status.code(),
393            stdout: stdout_delta,
394            stderr: stderr_delta,
395            duration,
396        }
397    }
398
399    fn add_definition(&mut self, snippet: String) {
400        self.definitions.push(snippet);
401    }
402
403    fn add_statement(&mut self, snippet: String) {
404        self.statements.push(snippet);
405    }
406
407    fn remove_last_definition(&mut self) {
408        let _ = self.definitions.pop();
409    }
410
411    fn remove_last_statement(&mut self) {
412        let _ = self.statements.pop();
413    }
414}
415
416impl LanguageSession for CppSession {
417    fn language_id(&self) -> &str {
418        "cpp"
419    }
420
421    fn eval(&mut self, code: &str) -> Result<ExecutionOutcome> {
422        let trimmed = code.trim();
423        if trimmed.is_empty() {
424            return Ok(ExecutionOutcome {
425                language: self.language_id().to_string(),
426                exit_code: None,
427                stdout: String::new(),
428                stderr: String::new(),
429                duration: Instant::now().elapsed(),
430            });
431        }
432
433        if trimmed.eq_ignore_ascii_case(":reset") {
434            self.reset_state()?;
435            return Ok(ExecutionOutcome {
436                language: self.language_id().to_string(),
437                exit_code: None,
438                stdout: String::new(),
439                stderr: String::new(),
440                duration: Duration::default(),
441            });
442        }
443
444        if trimmed.eq_ignore_ascii_case(":help") {
445            return Ok(ExecutionOutcome {
446                language: self.language_id().to_string(),
447                exit_code: None,
448                stdout:
449                    "C++ commands:\n  :reset - clear session state\n  :help  - show this message\n"
450                        .to_string(),
451                stderr: String::new(),
452                duration: Duration::default(),
453            });
454        }
455
456        if contains_main_definition(code) {
457            return self.run_standalone_program(code);
458        }
459
460        let classification = classify_snippet(trimmed);
461        match classification {
462            SnippetKind::Definition => {
463                self.add_definition(code.to_string());
464                let (output, duration) = self.compile_and_run()?;
465                if !output.status.success() {
466                    self.remove_last_definition();
467                }
468                Ok(self.diff_outputs(&output, duration))
469            }
470            SnippetKind::Expression => {
471                let wrapped = wrap_cpp_expression(trimmed);
472                self.add_statement(wrapped);
473                let (output, duration) = self.compile_and_run()?;
474                if !output.status.success() {
475                    self.remove_last_statement();
476                    return Ok(self.diff_outputs(&output, duration));
477                }
478                Ok(self.diff_outputs(&output, duration))
479            }
480            SnippetKind::Statement => {
481                let stmt = ensure_trailing_newline(code);
482                self.add_statement(stmt);
483                let (output, duration) = self.compile_and_run()?;
484                if !output.status.success() {
485                    self.remove_last_statement();
486                }
487                Ok(self.diff_outputs(&output, duration))
488            }
489        }
490    }
491
492    fn shutdown(&mut self) -> Result<()> {
493        Ok(())
494    }
495}
496
497#[derive(Debug, Clone, Copy, PartialEq, Eq)]
498enum SnippetKind {
499    Definition,
500    Statement,
501    Expression,
502}
503
504fn classify_snippet(code: &str) -> SnippetKind {
505    let trimmed = code.trim();
506    if trimmed.starts_with("#include")
507        || trimmed.starts_with("using ")
508        || trimmed.starts_with("namespace ")
509        || trimmed.starts_with("class ")
510        || trimmed.starts_with("struct ")
511        || trimmed.starts_with("enum ")
512        || trimmed.starts_with("template ")
513        || trimmed.ends_with("};")
514    {
515        return SnippetKind::Definition;
516    }
517
518    if trimmed.contains('{') && trimmed.contains('}') && trimmed.contains('(') {
519        const CONTROL_KEYWORDS: [&str; 8] =
520            ["if", "for", "while", "switch", "do", "else", "try", "catch"];
521        let first = trimmed.split_whitespace().next().unwrap_or("");
522        if !CONTROL_KEYWORDS.iter().any(|kw| {
523            first == *kw
524                || trimmed.starts_with(&format!("{} ", kw))
525                || trimmed.starts_with(&format!("{}(", kw))
526        }) {
527            return SnippetKind::Definition;
528        }
529    }
530
531    if is_cpp_expression(trimmed) {
532        return SnippetKind::Expression;
533    }
534
535    SnippetKind::Statement
536}
537
538fn is_cpp_expression(code: &str) -> bool {
539    if code.contains('\n') {
540        return false;
541    }
542    if code.ends_with(';') {
543        return false;
544    }
545    if code.starts_with("return ") {
546        return false;
547    }
548    if code.starts_with("if ")
549        || code.starts_with("for ")
550        || code.starts_with("while ")
551        || code.starts_with("switch ")
552        || code.starts_with("do ")
553        || code.starts_with("auto ")
554    {
555        return false;
556    }
557    if code.starts_with("std::") && code.contains('(') {
558        return false;
559    }
560    if code.starts_with("cout") || code.starts_with("cin") {
561        return false;
562    }
563    if code.starts_with('"') && code.ends_with('"') {
564        return true;
565    }
566    if code.parse::<f64>().is_ok() {
567        return true;
568    }
569    if code == "true" || code == "false" {
570        return true;
571    }
572    if code.contains("==") || code.contains("!=") || code.contains("<=") || code.contains(">=") {
573        return true;
574    }
575    if code.chars().any(|c| "+-*/%<>^|&".contains(c)) {
576        return true;
577    }
578    if code
579        .chars()
580        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.')
581    {
582        return true;
583    }
584    false
585}
586
587fn contains_main_definition(code: &str) -> bool {
588    let bytes = code.as_bytes();
589    let len = bytes.len();
590    let mut i = 0;
591    let mut in_line_comment = false;
592    let mut in_block_comment = false;
593    let mut in_string = false;
594    let mut string_delim = b'"';
595    let mut in_char = false;
596
597    while i < len {
598        let b = bytes[i];
599
600        if in_line_comment {
601            if b == b'\n' {
602                in_line_comment = false;
603            }
604            i += 1;
605            continue;
606        }
607
608        if in_block_comment {
609            if b == b'*' && i + 1 < len && bytes[i + 1] == b'/' {
610                in_block_comment = false;
611                i += 2;
612                continue;
613            }
614            i += 1;
615            continue;
616        }
617
618        if in_string {
619            if b == b'\\' {
620                i = (i + 2).min(len);
621                continue;
622            }
623            if b == string_delim {
624                in_string = false;
625            }
626            i += 1;
627            continue;
628        }
629
630        if in_char {
631            if b == b'\\' {
632                i = (i + 2).min(len);
633                continue;
634            }
635            if b == b'\'' {
636                in_char = false;
637            }
638            i += 1;
639            continue;
640        }
641
642        match b {
643            b'/' if i + 1 < len && bytes[i + 1] == b'/' => {
644                in_line_comment = true;
645                i += 2;
646                continue;
647            }
648            b'/' if i + 1 < len && bytes[i + 1] == b'*' => {
649                in_block_comment = true;
650                i += 2;
651                continue;
652            }
653            b'"' | b'\'' => {
654                if b == b'"' {
655                    in_string = true;
656                    string_delim = b;
657                } else {
658                    in_char = true;
659                }
660                i += 1;
661                continue;
662            }
663            b'm' if i + 4 <= len && &bytes[i..i + 4] == b"main" => {
664                if i > 0 {
665                    let prev = bytes[i - 1];
666                    if prev.is_ascii_alphanumeric() || prev == b'_' {
667                        i += 1;
668                        continue;
669                    }
670                }
671
672                let after_name = i + 4;
673                if after_name < len {
674                    let next = bytes[after_name];
675                    if next.is_ascii_alphanumeric() || next == b'_' {
676                        i += 1;
677                        continue;
678                    }
679                }
680
681                let mut j = after_name;
682                while j < len && bytes[j].is_ascii_whitespace() {
683                    j += 1;
684                }
685                if j >= len || bytes[j] != b'(' {
686                    i += 1;
687                    continue;
688                }
689
690                let mut depth = 1usize;
691                let mut k = j + 1;
692                let mut inner_line_comment = false;
693                let mut inner_block_comment = false;
694                let mut inner_string = false;
695                let mut inner_char = false;
696
697                while k < len {
698                    let ch = bytes[k];
699
700                    if inner_line_comment {
701                        if ch == b'\n' {
702                            inner_line_comment = false;
703                        }
704                        k += 1;
705                        continue;
706                    }
707
708                    if inner_block_comment {
709                        if ch == b'*' && k + 1 < len && bytes[k + 1] == b'/' {
710                            inner_block_comment = false;
711                            k += 2;
712                            continue;
713                        }
714                        k += 1;
715                        continue;
716                    }
717
718                    if inner_string {
719                        if ch == b'\\' {
720                            k = (k + 2).min(len);
721                            continue;
722                        }
723                        if ch == b'"' {
724                            inner_string = false;
725                        }
726                        k += 1;
727                        continue;
728                    }
729
730                    if inner_char {
731                        if ch == b'\\' {
732                            k = (k + 2).min(len);
733                            continue;
734                        }
735                        if ch == b'\'' {
736                            inner_char = false;
737                        }
738                        k += 1;
739                        continue;
740                    }
741
742                    match ch {
743                        b'/' if k + 1 < len && bytes[k + 1] == b'/' => {
744                            inner_line_comment = true;
745                            k += 2;
746                            continue;
747                        }
748                        b'/' if k + 1 < len && bytes[k + 1] == b'*' => {
749                            inner_block_comment = true;
750                            k += 2;
751                            continue;
752                        }
753                        b'"' => {
754                            inner_string = true;
755                            k += 1;
756                            continue;
757                        }
758                        b'\'' => {
759                            inner_char = true;
760                            k += 1;
761                            continue;
762                        }
763                        b'(' => {
764                            depth += 1;
765                        }
766                        b')' => {
767                            depth -= 1;
768                            k += 1;
769                            if depth == 0 {
770                                break;
771                            } else {
772                                continue;
773                            }
774                        }
775                        _ => {}
776                    }
777
778                    k += 1;
779                }
780
781                if depth != 0 {
782                    i += 1;
783                    continue;
784                }
785
786                let mut after = k;
787                loop {
788                    while after < len && bytes[after].is_ascii_whitespace() {
789                        after += 1;
790                    }
791                    if after + 1 < len && bytes[after] == b'/' && bytes[after + 1] == b'/' {
792                        after += 2;
793                        while after < len && bytes[after] != b'\n' {
794                            after += 1;
795                        }
796                        continue;
797                    }
798                    if after + 1 < len && bytes[after] == b'/' && bytes[after + 1] == b'*' {
799                        after += 2;
800                        while after + 1 < len {
801                            if bytes[after] == b'*' && bytes[after + 1] == b'/' {
802                                after += 2;
803                                break;
804                            }
805                            after += 1;
806                        }
807                        continue;
808                    }
809                    break;
810                }
811
812                while after < len {
813                    match bytes[after] {
814                        b'{' => return true,
815                        b';' => break,
816                        b'/' if after + 1 < len && bytes[after + 1] == b'/' => {
817                            after += 2;
818                            while after < len && bytes[after] != b'\n' {
819                                after += 1;
820                            }
821                        }
822                        b'/' if after + 1 < len && bytes[after + 1] == b'*' => {
823                            after += 2;
824                            while after + 1 < len {
825                                if bytes[after] == b'*' && bytes[after + 1] == b'/' {
826                                    after += 2;
827                                    break;
828                                }
829                                after += 1;
830                            }
831                        }
832                        b'"' => {
833                            after += 1;
834                            while after < len {
835                                if bytes[after] == b'"' {
836                                    after += 1;
837                                    break;
838                                }
839                                if bytes[after] == b'\\' {
840                                    after = (after + 2).min(len);
841                                } else {
842                                    after += 1;
843                                }
844                            }
845                        }
846                        b'\'' => {
847                            after += 1;
848                            while after < len {
849                                if bytes[after] == b'\'' {
850                                    after += 1;
851                                    break;
852                                }
853                                if bytes[after] == b'\\' {
854                                    after = (after + 2).min(len);
855                                } else {
856                                    after += 1;
857                                }
858                            }
859                        }
860                        b'-' if after + 1 < len && bytes[after + 1] == b'>' => {
861                            after += 2;
862                        }
863                        b'(' => {
864                            let mut depth = 1usize;
865                            after += 1;
866                            while after < len && depth > 0 {
867                                match bytes[after] {
868                                    b'(' => depth += 1,
869                                    b')' => depth -= 1,
870                                    b'"' => {
871                                        after += 1;
872                                        while after < len {
873                                            if bytes[after] == b'"' {
874                                                after += 1;
875                                                break;
876                                            }
877                                            if bytes[after] == b'\\' {
878                                                after = (after + 2).min(len);
879                                            } else {
880                                                after += 1;
881                                            }
882                                        }
883                                        continue;
884                                    }
885                                    b'\'' => {
886                                        after += 1;
887                                        while after < len {
888                                            if bytes[after] == b'\'' {
889                                                after += 1;
890                                                break;
891                                            }
892                                            if bytes[after] == b'\\' {
893                                                after = (after + 2).min(len);
894                                            } else {
895                                                after += 1;
896                                            }
897                                        }
898                                        continue;
899                                    }
900                                    _ => {}
901                                }
902                                after += 1;
903                            }
904                        }
905                        _ => {
906                            after += 1;
907                        }
908                    }
909                }
910            }
911            _ => {}
912        }
913
914        i += 1;
915    }
916
917    false
918}
919
920fn wrap_cpp_expression(code: &str) -> String {
921    format!("std::cout << ({code}) << std::endl;\n")
922}
923
924fn ensure_trailing_newline(code: &str) -> String {
925    let mut owned = code.to_string();
926    if !owned.ends_with('\n') {
927        owned.push('\n');
928    }
929    owned
930}
931
932fn diff_output(previous: &str, current: &str) -> String {
933    if let Some(stripped) = current.strip_prefix(previous) {
934        stripped.to_string()
935    } else {
936        current.to_string()
937    }
938}
939
940fn normalize_output(bytes: &[u8]) -> String {
941    String::from_utf8_lossy(bytes)
942        .replace("\r\n", "\n")
943        .replace('\r', "")
944}
945
946fn invoke_cpp_compiler(
947    compiler: &Path,
948    source: &Path,
949    output: &Path,
950) -> Result<std::process::Output> {
951    let mut cmd = Command::new(compiler);
952    cmd.arg(source)
953        .arg("-std=c++17")
954        .arg("-O0")
955        .arg("-Wall")
956        .arg("-Wextra")
957        .arg("-o")
958        .arg(output)
959        .stdout(Stdio::piped())
960        .stderr(Stdio::piped());
961    cmd.output().with_context(|| {
962        format!(
963            "failed to invoke {} to compile {}",
964            compiler.display(),
965            source.display()
966        )
967    })
968}
969
970fn run_cpp_binary(binary: &Path) -> Result<std::process::Output> {
971    let mut cmd = Command::new(binary);
972    cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
973    cmd.output()
974        .with_context(|| format!("failed to execute compiled binary {}", binary.display()))
975}