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 (preserves newline characters)
81///
82/// Returns the line including trailing newline (\n or \r\n).
83/// Returns empty string "" at EOF.
84/// Use `string-chomp` to remove trailing newlines if needed.
85///
86/// Stack effect: ( -- str )
87///
88/// # Safety
89/// Always safe to call
90#[unsafe(no_mangle)]
91pub unsafe extern "C" fn patch_seq_read_line(stack: Stack) -> Stack {
92    use std::io::BufRead;
93
94    let stdin = io::stdin();
95    let mut line = String::new();
96
97    stdin
98        .lock()
99        .read_line(&mut line)
100        .expect("read_line: failed to read from stdin (I/O error or EOF)");
101
102    // Preserve newlines - callers can use string-chomp if needed
103    unsafe { push(stack, Value::String(line.into())) }
104}
105
106/// Read a line from stdin with explicit EOF detection
107///
108/// Returns the line and a status flag:
109/// - ( line 1 ) on success (line includes trailing newline)
110/// - ( "" 0 ) at EOF
111///
112/// Stack effect: ( -- String Int )
113///
114/// The `+` suffix indicates this returns a result pattern (value + status).
115///
116/// # Safety
117/// Always safe to call
118#[unsafe(no_mangle)]
119pub unsafe extern "C" fn patch_seq_read_line_plus(stack: Stack) -> Stack {
120    use std::io::BufRead;
121
122    let stdin = io::stdin();
123    let mut line = String::new();
124
125    let bytes_read = stdin
126        .lock()
127        .read_line(&mut line)
128        .expect("read_line_safe: failed to read from stdin");
129
130    // bytes_read == 0 means EOF
131    let status = if bytes_read > 0 { 1i64 } else { 0i64 };
132
133    let stack = unsafe { push(stack, Value::String(line.into())) };
134    unsafe { push(stack, Value::Int(status)) }
135}
136
137/// Convert an integer to a string
138///
139/// Stack effect: ( Int -- String )
140///
141/// # Safety
142/// Stack must have an Int value on top
143#[unsafe(no_mangle)]
144pub unsafe extern "C" fn patch_seq_int_to_string(stack: Stack) -> Stack {
145    assert!(!stack.is_null(), "int_to_string: stack is empty");
146
147    let (rest, value) = unsafe { pop(stack) };
148
149    match value {
150        Value::Int(n) => unsafe { push(rest, Value::String(n.to_string().into())) },
151        _ => panic!("int_to_string: expected Int on stack, got {:?}", value),
152    }
153}
154
155/// Push a C string literal onto the stack (for compiler-generated code)
156///
157/// Stack effect: ( -- str )
158///
159/// # Safety
160/// The c_str pointer must be valid and null-terminated
161#[unsafe(no_mangle)]
162pub unsafe extern "C" fn patch_seq_push_string(stack: Stack, c_str: *const i8) -> Stack {
163    assert!(!c_str.is_null(), "push_string: null string pointer");
164
165    let s = unsafe {
166        CStr::from_ptr(c_str)
167            .to_str()
168            .expect("push_string: invalid UTF-8 in string literal")
169            .to_owned()
170    };
171
172    unsafe { push(stack, Value::String(s.into())) }
173}
174
175/// Push a SeqString value onto the stack
176///
177/// This is used when we already have a SeqString (e.g., from closures).
178/// Unlike push_string which takes a C string, this takes a SeqString by value.
179///
180/// Stack effect: ( -- String )
181///
182/// # Safety
183/// The SeqString must be valid. This is only called from LLVM-generated code, not actual C code.
184#[allow(improper_ctypes_definitions)]
185#[unsafe(no_mangle)]
186pub unsafe extern "C" fn patch_seq_push_seqstring(
187    stack: Stack,
188    seq_str: crate::seqstring::SeqString,
189) -> Stack {
190    unsafe { push(stack, Value::String(seq_str)) }
191}
192
193/// Exit the program with a status code
194///
195/// Stack effect: ( exit_code -- )
196///
197/// # Safety
198/// Stack must have an Int on top. Never returns.
199#[unsafe(no_mangle)]
200pub unsafe extern "C" fn patch_seq_exit_op(stack: Stack) -> ! {
201    assert!(!stack.is_null(), "exit_op: stack is empty");
202
203    let (_rest, value) = unsafe { pop(stack) };
204
205    match value {
206        Value::Int(code) => {
207            // Explicitly validate exit code is in Unix-compatible range
208            if !(EXIT_CODE_MIN..=EXIT_CODE_MAX).contains(&code) {
209                panic!(
210                    "exit_op: exit code must be in range {}-{}, got {}",
211                    EXIT_CODE_MIN, EXIT_CODE_MAX, code
212                );
213            }
214            std::process::exit(code as i32);
215        }
216        _ => panic!("exit_op: expected Int on stack, got {:?}", value),
217    }
218}
219
220// Public re-exports with short names for internal use
221pub use patch_seq_exit_op as exit_op;
222pub use patch_seq_int_to_string as int_to_string;
223pub use patch_seq_push_seqstring as push_seqstring;
224pub use patch_seq_push_string as push_string;
225pub use patch_seq_read_line as read_line;
226pub use patch_seq_read_line_plus as read_line_plus;
227pub use patch_seq_write_line as write_line;
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use crate::value::Value;
233    use std::ffi::CString;
234
235    #[test]
236    fn test_write_line() {
237        unsafe {
238            let stack = std::ptr::null_mut();
239            let stack = push(stack, Value::String("Hello, World!".into()));
240            let _stack = write_line(stack);
241        }
242    }
243
244    #[test]
245    fn test_push_string() {
246        unsafe {
247            let stack = std::ptr::null_mut();
248            let test_str = CString::new("Test").unwrap();
249            let stack = push_string(stack, test_str.as_ptr());
250
251            let (stack, value) = pop(stack);
252            assert_eq!(value, Value::String("Test".into()));
253            assert!(stack.is_null());
254        }
255    }
256
257    #[test]
258    fn test_empty_string() {
259        unsafe {
260            // Empty string should be handled correctly
261            let stack = std::ptr::null_mut();
262            let empty_str = CString::new("").unwrap();
263            let stack = push_string(stack, empty_str.as_ptr());
264
265            let (stack, value) = pop(stack);
266            assert_eq!(value, Value::String("".into()));
267            assert!(stack.is_null());
268
269            // Write empty string should work without panic
270            let stack = push(stack, Value::String("".into()));
271            let stack = write_line(stack);
272            assert!(stack.is_null());
273        }
274    }
275
276    #[test]
277    fn test_unicode_strings() {
278        unsafe {
279            // Test that Unicode strings are handled correctly
280            let stack = std::ptr::null_mut();
281            let unicode_str = CString::new("Hello, δΈ–η•Œ! 🌍").unwrap();
282            let stack = push_string(stack, unicode_str.as_ptr());
283
284            let (stack, value) = pop(stack);
285            assert_eq!(value, Value::String("Hello, δΈ–η•Œ! 🌍".into()));
286            assert!(stack.is_null());
287        }
288    }
289}