monty_types/io.rs
1//! Print-output plumbing: [`PrintStream`], [`PrintWriter`] and the
2//! [`PrintWriterCallback`] trait used by hosts to capture `print()` output.
3
4use std::borrow::Cow;
5
6use crate::{
7 exceptions::{ExcType, MontyException},
8 resource::ResourceError,
9};
10
11/// Default cap for [`PrintWriter::CollectString`] / [`PrintWriter::CollectStreams`]
12/// and the matching Python collectors.
13///
14/// Host-side print buffers sit outside [`crate::ResourceLimits::max_memory`];
15/// without a cap, a print loop can OOM the host while sandbox limits stay green.
16/// Pass `max_bytes: None` to opt out on trusted hosts.
17pub const DEFAULT_MAX_PRINT_COLLECT_BYTES: usize = 10 * 1024 * 1024;
18
19/// Identifies the output stream for a single print fragment.
20///
21/// Today the `print()` builtin only writes to `Stdout`. The `Stderr` variant is
22/// included for forward compatibility with a future `print(..., file=sys.stderr)`
23/// implementation so the collected-output API shape does not have to change when
24/// that lands.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum PrintStream {
27 /// Standard output — the default for every `print()` call today.
28 Stdout,
29 /// Standard error — reserved for future `print(..., file=sys.stderr)` support.
30 Stderr,
31}
32
33/// Output handler for the `print()` builtin function.
34///
35/// Provides common output modes as enum variants to avoid trait object overhead
36/// in the typical cases (stdout, disabled, collect). For custom output handling,
37/// use the `Callback` variant with a [`PrintWriterCallback`] implementation.
38///
39/// # Variants
40/// - `Disabled` — silently discards all output (useful for benchmarking or suppressing output).
41/// - `Stdout` — writes to standard output (the default behavior).
42/// - `CollectString` — accumulates output into a target `String` for programmatic access.
43/// No stream labels are preserved; every fragment is appended in the order it was emitted.
44/// The `Option<usize>` is an optional byte cap (`None` = unlimited); constructors
45/// [`collect_string`](Self::collect_string) / default Python collectors use
46/// [`DEFAULT_MAX_PRINT_COLLECT_BYTES`].
47/// - `CollectStreams` — accumulates output as `(stream, text)` pairs, merging consecutive
48/// same-stream fragments into one tuple. Each write to the same stream extends the
49/// trailing entry rather than producing a new one; a new tuple is only pushed when
50/// the stream changes. Same optional byte cap as `CollectString`.
51/// - `Callback` — delegates to a user-provided [`PrintWriterCallback`] implementation.
52pub enum PrintWriter<'a> {
53 /// Silently discard all output.
54 Disabled,
55 /// Write to standard output.
56 Stdout,
57 /// Collect all output into a single `String`, in emit order, with no stream labels.
58 ///
59 /// Second field: max collected bytes (`None` = unlimited). Exceeding raises
60 /// `MemoryError` with the same message as [`ResourceError::Memory`].
61 CollectString(&'a mut String, Option<usize>),
62 /// Collect all output as `(stream, text)` tuples.
63 ///
64 /// The builtin `print()` implementation calls `stdout_write` for each argument
65 /// and `stdout_push` for each separator/terminator. To avoid one tuple per
66 /// fragment, this variant appends to the trailing tuple when it already matches
67 /// the current stream; a new tuple is only pushed when the stream changes.
68 /// So long as every write targets the same stream (the status quo today, since
69 /// `print()` only writes to stdout), a single `print(a, b)` call produces one
70 /// `(Stdout, "a b\n")` entry — and consecutive prints with `end=''` likewise
71 /// merge into a single trailing entry.
72 ///
73 /// Second field: max collected bytes across all tuples (`None` = unlimited).
74 CollectStreams(&'a mut Vec<(PrintStream, String)>, Option<usize>),
75 /// Delegate to a custom callback.
76 Callback(&'a mut dyn PrintWriterCallback),
77}
78
79impl PrintWriter<'_> {
80 /// Collect into `buf` with the default [`DEFAULT_MAX_PRINT_COLLECT_BYTES`] cap.
81 pub fn collect_string(buf: &mut String) -> PrintWriter<'_> {
82 PrintWriter::CollectString(buf, Some(DEFAULT_MAX_PRINT_COLLECT_BYTES))
83 }
84
85 /// Collect into `buf` with the default [`DEFAULT_MAX_PRINT_COLLECT_BYTES`] cap.
86 pub fn collect_streams(buf: &mut Vec<(PrintStream, String)>) -> PrintWriter<'_> {
87 PrintWriter::CollectStreams(buf, Some(DEFAULT_MAX_PRINT_COLLECT_BYTES))
88 }
89
90 /// Creates a new `PrintWriter` that reborrows the same underlying target.
91 ///
92 /// This is useful in iterative execution (`start`/`resume` loops) where each
93 /// step takes `PrintWriter` by value but you want all steps to write to the
94 /// same output target. The original writer remains valid after the reborrowed
95 /// copy is dropped.
96 pub fn reborrow(&mut self) -> PrintWriter<'_> {
97 match self {
98 Self::Disabled => PrintWriter::Disabled,
99 Self::Stdout => PrintWriter::Stdout,
100 Self::CollectString(buf, max) => PrintWriter::CollectString(buf, *max),
101 Self::CollectStreams(buf, max) => PrintWriter::CollectStreams(buf, *max),
102 Self::Callback(cb) => PrintWriter::Callback(&mut **cb),
103 }
104 }
105
106 /// Called once for each formatted argument passed to `print()`.
107 ///
108 /// This method writes only the given argument's text, without adding
109 /// separators or a trailing newline. Separators (spaces) and the final
110 /// terminator (newline) are emitted via [`stdout_push`](Self::stdout_push).
111 pub fn stdout_write(&mut self, output: Cow<'_, str>) -> Result<(), MontyException> {
112 match self {
113 Self::Disabled => Ok(()),
114 Self::Stdout => {
115 print!("{output}");
116 Ok(())
117 }
118 Self::CollectString(buf, max_bytes) => {
119 check_print_collect_limit(buf.len(), output.len(), *max_bytes)?;
120 buf.push_str(&output);
121 Ok(())
122 }
123 Self::CollectStreams(buf, max_bytes) => append_streams_str(buf, PrintStream::Stdout, &output, *max_bytes),
124 Self::Callback(cb) => cb.stdout_write(output),
125 }
126 }
127
128 /// Appends a single character to the output.
129 ///
130 /// Generally called to add spaces (separators) and newlines (terminators)
131 /// within print output.
132 pub fn stdout_push(&mut self, end: char) -> Result<(), MontyException> {
133 match self {
134 Self::Disabled => Ok(()),
135 Self::Stdout => {
136 print!("{end}");
137 Ok(())
138 }
139 Self::CollectString(buf, max_bytes) => {
140 check_print_collect_limit(buf.len(), end.len_utf8(), *max_bytes)?;
141 buf.push(end);
142 Ok(())
143 }
144 Self::CollectStreams(buf, max_bytes) => append_streams_char(buf, PrintStream::Stdout, end, *max_bytes),
145 Self::Callback(cb) => cb.stdout_push(end),
146 }
147 }
148}
149
150/// Rejects a collect-buffer growth that would exceed `max_bytes`.
151///
152/// `None` means unlimited. On overflow, returns the same `MemoryError` message
153/// as [`ResourceError::Memory`] so hosts see one familiar limit string.
154pub fn check_print_collect_limit(
155 current_len: usize,
156 add: usize,
157 max_bytes: Option<usize>,
158) -> Result<(), MontyException> {
159 let Some(limit) = max_bytes else {
160 return Ok(());
161 };
162 let used = current_len.saturating_add(add);
163 if used > limit {
164 Err(MontyException::new(
165 ExcType::MemoryError,
166 Some(ResourceError::Memory { limit, used }.to_string()),
167 ))
168 } else {
169 Ok(())
170 }
171}
172
173/// Total UTF-8 bytes across all collect-streams tuples.
174fn streams_byte_len(buf: &[(PrintStream, String)]) -> usize {
175 buf.iter().map(|(_, s)| s.len()).sum()
176}
177
178/// Appends a string fragment to the collect-streams buffer, merging into the
179/// trailing tuple when the stream matches.
180fn append_streams_str(
181 buf: &mut Vec<(PrintStream, String)>,
182 stream: PrintStream,
183 text: &str,
184 max_bytes: Option<usize>,
185) -> Result<(), MontyException> {
186 check_print_collect_limit(streams_byte_len(buf), text.len(), max_bytes)?;
187 match buf.last_mut() {
188 Some((s, existing)) if *s == stream => existing.push_str(text),
189 _ => buf.push((stream, text.to_owned())),
190 }
191 Ok(())
192}
193
194/// Appends a single character to the collect-streams buffer, merging into the
195/// trailing tuple when the stream matches.
196fn append_streams_char(
197 buf: &mut Vec<(PrintStream, String)>,
198 stream: PrintStream,
199 ch: char,
200 max_bytes: Option<usize>,
201) -> Result<(), MontyException> {
202 check_print_collect_limit(streams_byte_len(buf), ch.len_utf8(), max_bytes)?;
203 match buf.last_mut() {
204 Some((s, existing)) if *s == stream => existing.push(ch),
205 _ => buf.push((stream, String::from(ch))),
206 }
207 Ok(())
208}
209
210/// Trait for custom output handling from the `print()` builtin function.
211///
212/// Implement this trait and pass it via [`PrintWriter::Callback`] to capture
213/// or redirect print output from sandboxed Python code.
214pub trait PrintWriterCallback {
215 /// Called once for each formatted argument passed to `print()`.
216 ///
217 /// This method is responsible for writing only the given argument's text, and must
218 /// not add separators or a trailing newline. Separators (such as spaces) and the
219 /// final terminator (such as a newline) are emitted via [`stdout_push`](Self::stdout_push).
220 ///
221 /// # Arguments
222 /// * `output` - The formatted output string for a single argument (without
223 /// separators or trailing newline).
224 fn stdout_write(&mut self, output: Cow<'_, str>) -> Result<(), MontyException>;
225
226 /// Add a single character to stdout.
227 ///
228 /// Generally called to add spaces and newlines within print output.
229 ///
230 /// # Arguments
231 /// * `end` - The character to print after the formatted output.
232 fn stdout_push(&mut self, end: char) -> Result<(), MontyException>;
233}