grift_core/io.rs
1//! I/O trait boundary for the Grift Scheme evaluator.
2//!
3//! This module defines the [`IoProvider`] trait, which serves as the boundary
4//! between the pure `no_std` evaluator and platform-specific I/O implementations.
5//!
6//! The `grift_std` crate provides a standard implementation using Rust's `std::io`.
7//!
8//! ## Port Model
9//!
10//! R7RS defines I/O in terms of *ports*. A [`PortId`] identifies a port,
11//! with well-known constants for standard input, output, and error.
12
13use grift_arena::ArenaIndex;
14
15// ============================================================================
16// Port Identifier
17// ============================================================================
18
19/// Identifies an I/O port.
20///
21/// Standard ports ([`STDIN`](PortId::STDIN), [`STDOUT`](PortId::STDOUT),
22/// [`STDERR`](PortId::STDERR)) are pre-defined. Implementations may allocate
23/// additional ports for file or string I/O.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct PortId(pub usize);
26
27impl PortId {
28 /// Standard input port.
29 pub const STDIN: PortId = PortId(0);
30 /// Standard output port.
31 pub const STDOUT: PortId = PortId(1);
32 /// Standard error port.
33 pub const STDERR: PortId = PortId(2);
34}
35
36// ============================================================================
37// I/O Error
38// ============================================================================
39
40/// Error kinds for I/O operations in `no_std` environments.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum IoErrorKind {
43 /// End of file reached.
44 Eof,
45 /// Port not found or invalid.
46 InvalidPort,
47 /// Write operation failed.
48 WriteFailed,
49 /// Read operation failed.
50 ReadFailed,
51 /// Port is closed.
52 PortClosed,
53 /// Operation not supported on this port.
54 Unsupported,
55}
56
57/// Result type for I/O operations.
58pub type IoResult<T> = Result<T, IoErrorKind>;
59
60// ============================================================================
61// IoProvider Trait
62// ============================================================================
63
64/// Trait for providing I/O operations to the evaluator.
65///
66/// This trait defines the boundary between the pure `no_std` evaluator
67/// and platform-specific I/O implementations. The `grift_std` crate
68/// provides a standard implementation backed by Rust's `std::io`.
69///
70/// # Implementor Notes
71///
72/// * [`PortId::STDIN`], [`PortId::STDOUT`], and [`PortId::STDERR`] should
73/// always be accepted as valid ports.
74/// * Implementations may support additional dynamically-opened ports
75/// (e.g. file ports) by returning fresh [`PortId`] values.
76pub trait IoProvider {
77 /// Read a single character from the specified input port.
78 fn read_char(&mut self, port: PortId) -> IoResult<char>;
79
80 /// Peek at the next character without consuming it.
81 fn peek_char(&mut self, port: PortId) -> IoResult<char>;
82
83 /// Return `true` if a character is ready on the input port.
84 fn char_ready(&mut self, port: PortId) -> IoResult<bool>;
85
86 /// Write a single character to the specified output port.
87 fn write_char(&mut self, port: PortId, c: char) -> IoResult<()>;
88
89 /// Write a string slice to the specified output port.
90 fn write_str(&mut self, port: PortId, s: &str) -> IoResult<()>;
91
92 /// Flush the specified output port.
93 fn flush(&mut self, port: PortId) -> IoResult<()>;
94
95 /// Close the specified port.
96 fn close_port(&mut self, port: PortId) -> IoResult<()>;
97
98 /// Return `true` if the port is an input port.
99 fn is_input_port(&self, port: PortId) -> bool;
100
101 /// Return `true` if the port is an output port.
102 fn is_output_port(&self, port: PortId) -> bool;
103
104 /// Return `true` if the port is still open.
105 /// Default: returns `true` (assumes ports are open unless overridden).
106 fn is_port_open(&self, _port: PortId) -> bool {
107 true
108 }
109
110 /// Return `true` if the port is a textual port.
111 /// Default: returns `true` for any valid input or output port.
112 fn is_textual_port(&self, port: PortId) -> bool {
113 self.is_input_port(port) || self.is_output_port(port)
114 }
115
116 /// Return `true` if the port is a binary port.
117 /// Default: returns `false` (all ports are textual by default).
118 fn is_binary_port(&self, _port: PortId) -> bool {
119 false
120 }
121
122 /// Open an input port that reads from the given string.
123 ///
124 /// Returns a fresh [`PortId`] for the new port.
125 /// Default: returns [`IoErrorKind::Unsupported`].
126 fn open_input_string(&mut self, _s: &str) -> IoResult<PortId> {
127 Err(IoErrorKind::Unsupported)
128 }
129
130 /// Open an output port that accumulates characters into a string buffer.
131 ///
132 /// Returns a fresh [`PortId`] for the new port.
133 /// Default: returns [`IoErrorKind::Unsupported`].
134 fn open_output_string(&mut self) -> IoResult<PortId> {
135 Err(IoErrorKind::Unsupported)
136 }
137
138 /// Retrieve the accumulated string from an output string port.
139 ///
140 /// The port must have been created by [`open_output_string`](Self::open_output_string).
141 /// Default: returns [`IoErrorKind::Unsupported`].
142 fn get_output_string(&self, _port: PortId) -> IoResult<&str> {
143 Err(IoErrorKind::Unsupported)
144 }
145
146 // ----------------------------------------------------------------
147 // File system operations (R7RS §6.13)
148 // ----------------------------------------------------------------
149
150 /// Check whether a file exists at the given path.
151 /// Default: returns [`IoErrorKind::Unsupported`].
152 fn file_exists(&self, _path: &str) -> IoResult<bool> {
153 Err(IoErrorKind::Unsupported)
154 }
155
156 /// Delete the file at the given path.
157 /// Default: returns [`IoErrorKind::Unsupported`].
158 fn delete_file(&mut self, _path: &str) -> IoResult<()> {
159 Err(IoErrorKind::Unsupported)
160 }
161
162 /// Read the entire contents of a file as a string.
163 /// Default: returns [`IoErrorKind::Unsupported`].
164 fn read_file(&mut self, _path: &str) -> IoResult<&str> {
165 Err(IoErrorKind::Unsupported)
166 }
167
168 // ----------------------------------------------------------------
169 // Process / environment operations (R7RS §6.14)
170 // ----------------------------------------------------------------
171
172 /// Return the number of command-line arguments.
173 /// Default: returns [`IoErrorKind::Unsupported`].
174 fn command_line_count(&self) -> IoResult<usize> {
175 Err(IoErrorKind::Unsupported)
176 }
177
178 /// Return the command-line argument at the given index.
179 /// Default: returns [`IoErrorKind::Unsupported`].
180 fn command_line_arg(&self, _index: usize) -> IoResult<&str> {
181 Err(IoErrorKind::Unsupported)
182 }
183
184 /// Retrieve the value of an environment variable by name.
185 /// Returns `Ok(Some(value))` if found, `Ok(None)` if not set.
186 /// Default: returns [`IoErrorKind::Unsupported`].
187 fn get_environment_variable(&mut self, _name: &str) -> IoResult<Option<&str>> {
188 Err(IoErrorKind::Unsupported)
189 }
190
191 /// Return the number of environment variables.
192 /// Must be called before [`environment_variable_at`](Self::environment_variable_at)
193 /// to snapshot the current environment.
194 /// Default: returns [`IoErrorKind::Unsupported`].
195 fn environment_variables_count(&mut self) -> IoResult<usize> {
196 Err(IoErrorKind::Unsupported)
197 }
198
199 /// Return the environment variable name and value at the given index.
200 /// [`environment_variables_count`](Self::environment_variables_count) must be
201 /// called first to snapshot the environment.
202 /// Default: returns [`IoErrorKind::Unsupported`].
203 fn environment_variable_at(&self, _index: usize) -> IoResult<(&str, &str)> {
204 Err(IoErrorKind::Unsupported)
205 }
206
207 /// Exit the process with the given status code.
208 /// Default: returns [`IoErrorKind::Unsupported`].
209 fn exit_process(&mut self, _code: i32) -> IoResult<()> {
210 Err(IoErrorKind::Unsupported)
211 }
212
213 /// Emergency exit the process (no cleanup) with the given status code.
214 /// Default: returns [`IoErrorKind::Unsupported`].
215 fn emergency_exit_process(&mut self, _code: i32) -> IoResult<()> {
216 Err(IoErrorKind::Unsupported)
217 }
218}
219
220/// A no-op I/O provider that silently discards all output and returns
221/// [`IoErrorKind::Unsupported`] for reads.
222///
223/// Useful as a default when no I/O back-end is configured.
224pub struct NullIoProvider;
225
226impl IoProvider for NullIoProvider {
227 fn read_char(&mut self, _port: PortId) -> IoResult<char> {
228 Err(IoErrorKind::Unsupported)
229 }
230
231 fn peek_char(&mut self, _port: PortId) -> IoResult<char> {
232 Err(IoErrorKind::Unsupported)
233 }
234
235 fn char_ready(&mut self, _port: PortId) -> IoResult<bool> {
236 Err(IoErrorKind::Unsupported)
237 }
238
239 fn write_char(&mut self, _port: PortId, _c: char) -> IoResult<()> {
240 Ok(())
241 }
242
243 fn write_str(&mut self, _port: PortId, _s: &str) -> IoResult<()> {
244 Ok(())
245 }
246
247 fn flush(&mut self, _port: PortId) -> IoResult<()> {
248 Ok(())
249 }
250
251 fn close_port(&mut self, _port: PortId) -> IoResult<()> {
252 Ok(())
253 }
254
255 fn is_input_port(&self, _port: PortId) -> bool {
256 false
257 }
258
259 fn is_output_port(&self, _port: PortId) -> bool {
260 false
261 }
262}
263
264// ============================================================================
265// Display helper (for use with IoProvider)
266// ============================================================================
267
268/// Write a [`Value`](crate::Value) through an [`IoProvider`] port.
269///
270/// This is a convenience for the evaluator's `display` / `write` builtins
271/// to emit a value through the I/O abstraction without requiring `alloc`.
272/// The `idx` is an arena index that the caller can format into the port.
273///
274/// Implementations of higher-level display logic live in the evaluator
275/// (or in `grift_std`), but this marker type can carry the index through
276/// the trait boundary.
277#[derive(Debug, Clone, Copy)]
278pub struct DisplayPort {
279 /// The arena index of the value to display.
280 pub value: ArenaIndex,
281 /// The target port.
282 pub port: PortId,
283}