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