seq_runtime/io.rs
1//! I/O Operations for Seq
2//!
3//! These functions are exported with C ABI for LLVM codegen to call.
4//!
5//! # Safety Contract
6//!
7//! **IMPORTANT:** These functions are designed to be called ONLY by compiler-generated code,
8//! not by end users or arbitrary C code. The compiler is responsible for:
9//!
10//! - Ensuring stack has correct types (verified by type checker)
11//! - Passing valid, null-terminated C strings to `push_string`
12//! - Never calling these functions directly from user code
13//!
14//! # String Handling
15//!
16//! String literals from the compiler must be valid UTF-8 C strings (null-terminated).
17//! Currently, each string literal is allocated as an owned `String`. See
18//! `docs/STRING_INTERNING_DESIGN.md` for discussion of future optimizations
19//! (interning, static references, etc.).
20
21use crate::stack::{Stack, pop, push};
22use crate::value::Value;
23use std::ffi::CStr;
24use std::io;
25use std::sync::LazyLock;
26
27/// Coroutine-aware stdout mutex.
28/// Uses may::sync::Mutex which yields the coroutine when contended instead of blocking the OS thread.
29/// By serializing access to stdout, we prevent RefCell borrow panics that occur when multiple
30/// coroutines on the same thread try to access stdout's internal RefCell concurrently.
31static STDOUT_MUTEX: LazyLock<may::sync::Mutex<()>> = LazyLock::new(|| may::sync::Mutex::new(()));
32
33/// Valid exit code range for Unix compatibility
34const EXIT_CODE_MIN: i64 = 0;
35const EXIT_CODE_MAX: i64 = 255;
36
37/// Write a string to stdout followed by a newline
38///
39/// Stack effect: ( str -- )
40///
41/// # Safety
42/// Stack must have a String value on top
43///
44/// # Concurrency
45/// Uses may::sync::Mutex to serialize stdout writes from multiple strands.
46/// When the mutex is contended, the strand yields to the scheduler (doesn't block the OS thread).
47/// This prevents RefCell borrow panics when multiple strands write concurrently.
48#[unsafe(no_mangle)]
49pub unsafe extern "C" fn patch_seq_write_line(stack: Stack) -> Stack {
50 assert!(!stack.is_null(), "write_line: stack is empty");
51
52 let (rest, value) = unsafe { pop(stack) };
53
54 match value {
55 Value::String(s) => {
56 // Acquire coroutine-aware mutex (yields if contended, doesn't block)
57 // This serializes access to stdout
58 let _guard = STDOUT_MUTEX.lock().unwrap();
59
60 // Write directly to fd 1 using libc to avoid Rust's std::io::stdout() RefCell.
61 // Rust's standard I/O uses RefCell which panics on concurrent access from
62 // multiple coroutines on the same thread.
63 let str_slice = s.as_str();
64 let newline = b"\n";
65 unsafe {
66 libc::write(
67 1,
68 str_slice.as_ptr() as *const libc::c_void,
69 str_slice.len(),
70 );
71 libc::write(1, newline.as_ptr() as *const libc::c_void, newline.len());
72 }
73
74 rest
75 }
76 _ => panic!("write_line: expected String on stack, got {:?}", value),
77 }
78}
79
80/// Read a line from stdin
81///
82/// Returns the line including trailing newline.
83/// Returns empty string "" at EOF.
84/// Use `string-chomp` to remove trailing newlines if needed.
85///
86/// # Line Ending Normalization
87///
88/// Line endings are normalized to `\n` regardless of platform. Windows-style
89/// `\r\n` endings are converted to `\n`. This ensures consistent behavior
90/// across different operating systems.
91///
92/// Stack effect: ( -- str )
93///
94/// # Safety
95/// Always safe to call
96#[unsafe(no_mangle)]
97pub unsafe extern "C" fn patch_seq_read_line(stack: Stack) -> Stack {
98 use std::io::BufRead;
99
100 let stdin = io::stdin();
101 let mut line = String::new();
102
103 stdin
104 .lock()
105 .read_line(&mut line)
106 .expect("read_line: failed to read from stdin (I/O error or EOF)");
107
108 // Normalize line endings: \r\n -> \n
109 if line.ends_with("\r\n") {
110 line.pop(); // remove \n
111 line.pop(); // remove \r
112 line.push('\n'); // add back \n
113 }
114
115 unsafe { push(stack, Value::String(line.into())) }
116}
117
118/// Read a line from stdin with explicit EOF detection
119///
120/// Returns the line and a status flag:
121/// - ( line 1 ) on success (line includes trailing newline)
122/// - ( "" 0 ) at EOF
123///
124/// Stack effect: ( -- String Int )
125///
126/// The `+` suffix indicates this returns a result pattern (value + status).
127///
128/// # Line Ending Normalization
129///
130/// Line endings are normalized to `\n` regardless of platform. Windows-style
131/// `\r\n` endings are converted to `\n`. This ensures consistent behavior
132/// across different operating systems.
133///
134/// # Safety
135/// Always safe to call
136#[unsafe(no_mangle)]
137pub unsafe extern "C" fn patch_seq_read_line_plus(stack: Stack) -> Stack {
138 use std::io::BufRead;
139
140 let stdin = io::stdin();
141 let mut line = String::new();
142
143 let bytes_read = stdin
144 .lock()
145 .read_line(&mut line)
146 .expect("read_line_safe: failed to read from stdin");
147
148 // Normalize line endings: \r\n -> \n
149 if line.ends_with("\r\n") {
150 line.pop(); // remove \n
151 line.pop(); // remove \r
152 line.push('\n'); // add back \n
153 }
154
155 // bytes_read == 0 means EOF
156 let status = if bytes_read > 0 { 1i64 } else { 0i64 };
157
158 let stack = unsafe { push(stack, Value::String(line.into())) };
159 unsafe { push(stack, Value::Int(status)) }
160}
161
162/// Convert an integer to a string
163///
164/// Stack effect: ( Int -- String )
165///
166/// # Safety
167/// Stack must have an Int value on top
168#[unsafe(no_mangle)]
169pub unsafe extern "C" fn patch_seq_int_to_string(stack: Stack) -> Stack {
170 assert!(!stack.is_null(), "int_to_string: stack is empty");
171
172 let (rest, value) = unsafe { pop(stack) };
173
174 match value {
175 Value::Int(n) => unsafe { push(rest, Value::String(n.to_string().into())) },
176 _ => panic!("int_to_string: expected Int on stack, got {:?}", value),
177 }
178}
179
180/// Push a C string literal onto the stack (for compiler-generated code)
181///
182/// Stack effect: ( -- str )
183///
184/// # Safety
185/// The c_str pointer must be valid and null-terminated
186#[unsafe(no_mangle)]
187pub unsafe extern "C" fn patch_seq_push_string(stack: Stack, c_str: *const i8) -> Stack {
188 assert!(!c_str.is_null(), "push_string: null string pointer");
189
190 let s = unsafe {
191 CStr::from_ptr(c_str)
192 .to_str()
193 .expect("push_string: invalid UTF-8 in string literal")
194 .to_owned()
195 };
196
197 unsafe { push(stack, Value::String(s.into())) }
198}
199
200/// Push a SeqString value onto the stack
201///
202/// This is used when we already have a SeqString (e.g., from closures).
203/// Unlike push_string which takes a C string, this takes a SeqString by value.
204///
205/// Stack effect: ( -- String )
206///
207/// # Safety
208/// The SeqString must be valid. This is only called from LLVM-generated code, not actual C code.
209#[allow(improper_ctypes_definitions)]
210#[unsafe(no_mangle)]
211pub unsafe extern "C" fn patch_seq_push_seqstring(
212 stack: Stack,
213 seq_str: crate::seqstring::SeqString,
214) -> Stack {
215 unsafe { push(stack, Value::String(seq_str)) }
216}
217
218/// Exit the program with a status code
219///
220/// Stack effect: ( exit_code -- )
221///
222/// # Safety
223/// Stack must have an Int on top. Never returns.
224#[unsafe(no_mangle)]
225pub unsafe extern "C" fn patch_seq_exit_op(stack: Stack) -> ! {
226 assert!(!stack.is_null(), "exit_op: stack is empty");
227
228 let (_rest, value) = unsafe { pop(stack) };
229
230 match value {
231 Value::Int(code) => {
232 // Explicitly validate exit code is in Unix-compatible range
233 if !(EXIT_CODE_MIN..=EXIT_CODE_MAX).contains(&code) {
234 panic!(
235 "exit_op: exit code must be in range {}-{}, got {}",
236 EXIT_CODE_MIN, EXIT_CODE_MAX, code
237 );
238 }
239 std::process::exit(code as i32);
240 }
241 _ => panic!("exit_op: expected Int on stack, got {:?}", value),
242 }
243}
244
245// Public re-exports with short names for internal use
246pub use patch_seq_exit_op as exit_op;
247pub use patch_seq_int_to_string as int_to_string;
248pub use patch_seq_push_seqstring as push_seqstring;
249pub use patch_seq_push_string as push_string;
250pub use patch_seq_read_line as read_line;
251pub use patch_seq_read_line_plus as read_line_plus;
252pub use patch_seq_write_line as write_line;
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use crate::value::Value;
258 use std::ffi::CString;
259
260 #[test]
261 fn test_write_line() {
262 unsafe {
263 let stack = std::ptr::null_mut();
264 let stack = push(stack, Value::String("Hello, World!".into()));
265 let _stack = write_line(stack);
266 }
267 }
268
269 #[test]
270 fn test_push_string() {
271 unsafe {
272 let stack = std::ptr::null_mut();
273 let test_str = CString::new("Test").unwrap();
274 let stack = push_string(stack, test_str.as_ptr());
275
276 let (stack, value) = pop(stack);
277 assert_eq!(value, Value::String("Test".into()));
278 assert!(stack.is_null());
279 }
280 }
281
282 #[test]
283 fn test_empty_string() {
284 unsafe {
285 // Empty string should be handled correctly
286 let stack = std::ptr::null_mut();
287 let empty_str = CString::new("").unwrap();
288 let stack = push_string(stack, empty_str.as_ptr());
289
290 let (stack, value) = pop(stack);
291 assert_eq!(value, Value::String("".into()));
292 assert!(stack.is_null());
293
294 // Write empty string should work without panic
295 let stack = push(stack, Value::String("".into()));
296 let stack = write_line(stack);
297 assert!(stack.is_null());
298 }
299 }
300
301 #[test]
302 fn test_unicode_strings() {
303 unsafe {
304 // Test that Unicode strings are handled correctly
305 let stack = std::ptr::null_mut();
306 let unicode_str = CString::new("Hello, δΈη! π").unwrap();
307 let stack = push_string(stack, unicode_str.as_ptr());
308
309 let (stack, value) = pop(stack);
310 assert_eq!(value, Value::String("Hello, δΈη! π".into()));
311 assert!(stack.is_null());
312 }
313 }
314}