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