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