seq_runtime/
file.rs

1//! File I/O Operations for Seq
2//!
3//! Provides file reading operations for Seq programs.
4//!
5//! # Usage from Seq
6//!
7//! ```seq
8//! "config.json" file-slurp  # ( String -- String ) read entire file
9//! "config.json" file-exists?  # ( String -- Int ) 1 if exists, 0 otherwise
10//! ```
11//!
12//! # Example
13//!
14//! ```seq
15//! : main ( -- Int )
16//!   "config.json" file-exists? if
17//!     "config.json" file-slurp write_line
18//!   else
19//!     "File not found" write_line
20//!   then
21//!   0
22//! ;
23//! ```
24
25use crate::stack::{Stack, pop, push};
26use crate::value::Value;
27use std::fs;
28use std::path::Path;
29
30/// Read entire file contents as a string
31///
32/// Stack effect: ( String -- String )
33///
34/// Takes a file path, reads the entire file, and returns its contents.
35/// Panics if the file cannot be read (doesn't exist, no permission, not UTF-8, etc.)
36///
37/// # Safety
38/// - `stack` must be a valid, non-null stack pointer with a String value on top
39/// - Caller must ensure stack is not concurrently modified
40#[unsafe(no_mangle)]
41pub unsafe extern "C" fn patch_seq_file_slurp(stack: Stack) -> Stack {
42    assert!(!stack.is_null(), "file-slurp: stack is empty");
43
44    let (rest, value) = unsafe { pop(stack) };
45
46    match value {
47        Value::String(path) => {
48            let contents = fs::read_to_string(path.as_str()).unwrap_or_else(|e| {
49                panic!("file-slurp: failed to read '{}': {}", path.as_str(), e)
50            });
51
52            unsafe { push(rest, Value::String(contents.into())) }
53        }
54        _ => panic!("file-slurp: expected String path on stack, got {:?}", value),
55    }
56}
57
58/// Check if a file exists
59///
60/// Stack effect: ( String -- Int )
61///
62/// Takes a file path and returns 1 if the file exists, 0 otherwise.
63///
64/// # Safety
65/// - `stack` must be a valid, non-null stack pointer with a String value on top
66/// - Caller must ensure stack is not concurrently modified
67#[unsafe(no_mangle)]
68pub unsafe extern "C" fn patch_seq_file_exists(stack: Stack) -> Stack {
69    assert!(!stack.is_null(), "file-exists?: stack is empty");
70
71    let (rest, value) = unsafe { pop(stack) };
72
73    match value {
74        Value::String(path) => {
75            let exists = if Path::new(path.as_str()).exists() {
76                1i64
77            } else {
78                0i64
79            };
80
81            unsafe { push(rest, Value::Int(exists)) }
82        }
83        _ => panic!(
84            "file-exists?: expected String path on stack, got {:?}",
85            value
86        ),
87    }
88}
89
90/// Read entire file contents as a string, with error handling
91///
92/// Stack effect: ( String -- String Int )
93///
94/// Takes a file path, attempts to read the entire file.
95/// Returns (contents 1) on success, or ("" 0) on failure.
96/// Failure cases: file not found, permission denied, not valid UTF-8, etc.
97///
98/// # Safety
99/// - `stack` must be a valid, non-null stack pointer with a String value on top
100/// - Caller must ensure stack is not concurrently modified
101#[unsafe(no_mangle)]
102pub unsafe extern "C" fn patch_seq_file_slurp_safe(stack: Stack) -> Stack {
103    assert!(!stack.is_null(), "file-slurp-safe: stack is empty");
104
105    let (rest, value) = unsafe { pop(stack) };
106
107    match value {
108        Value::String(path) => match fs::read_to_string(path.as_str()) {
109            Ok(contents) => {
110                let stack = unsafe { push(rest, Value::String(contents.into())) };
111                unsafe { push(stack, Value::Int(1)) }
112            }
113            Err(_) => {
114                let stack = unsafe { push(rest, Value::String("".into())) };
115                unsafe { push(stack, Value::Int(0)) }
116            }
117        },
118        _ => panic!(
119            "file-slurp-safe: expected String path on stack, got {:?}",
120            value
121        ),
122    }
123}
124
125// Public re-exports
126pub use patch_seq_file_exists as file_exists;
127pub use patch_seq_file_slurp as file_slurp;
128pub use patch_seq_file_slurp_safe as file_slurp_safe;
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use std::io::Write;
134    use tempfile::NamedTempFile;
135
136    #[test]
137    fn test_file_slurp() {
138        // Create a temporary file with known contents
139        let mut temp_file = NamedTempFile::new().unwrap();
140        writeln!(temp_file, "Hello, file!").unwrap();
141        let path = temp_file.path().to_str().unwrap().to_string();
142
143        unsafe {
144            let stack = std::ptr::null_mut();
145            let stack = push(stack, Value::String(path.into()));
146            let stack = patch_seq_file_slurp(stack);
147
148            let (stack, value) = pop(stack);
149            match value {
150                Value::String(s) => assert_eq!(s.as_str().trim(), "Hello, file!"),
151                _ => panic!("Expected String"),
152            }
153            assert!(stack.is_null());
154        }
155    }
156
157    #[test]
158    fn test_file_exists_true() {
159        let temp_file = NamedTempFile::new().unwrap();
160        let path = temp_file.path().to_str().unwrap().to_string();
161
162        unsafe {
163            let stack = std::ptr::null_mut();
164            let stack = push(stack, Value::String(path.into()));
165            let stack = patch_seq_file_exists(stack);
166
167            let (stack, value) = pop(stack);
168            assert_eq!(value, Value::Int(1));
169            assert!(stack.is_null());
170        }
171    }
172
173    #[test]
174    fn test_file_exists_false() {
175        unsafe {
176            let stack = std::ptr::null_mut();
177            let stack = push(stack, Value::String("/nonexistent/path/to/file.txt".into()));
178            let stack = patch_seq_file_exists(stack);
179
180            let (stack, value) = pop(stack);
181            assert_eq!(value, Value::Int(0));
182            assert!(stack.is_null());
183        }
184    }
185
186    #[test]
187    fn test_file_slurp_utf8() {
188        let mut temp_file = NamedTempFile::new().unwrap();
189        write!(temp_file, "Hello, δΈ–η•Œ! 🌍").unwrap();
190        let path = temp_file.path().to_str().unwrap().to_string();
191
192        unsafe {
193            let stack = std::ptr::null_mut();
194            let stack = push(stack, Value::String(path.into()));
195            let stack = patch_seq_file_slurp(stack);
196
197            let (stack, value) = pop(stack);
198            match value {
199                Value::String(s) => assert_eq!(s.as_str(), "Hello, δΈ–η•Œ! 🌍"),
200                _ => panic!("Expected String"),
201            }
202            assert!(stack.is_null());
203        }
204    }
205
206    #[test]
207    fn test_file_slurp_empty() {
208        let temp_file = NamedTempFile::new().unwrap();
209        let path = temp_file.path().to_str().unwrap().to_string();
210
211        unsafe {
212            let stack = std::ptr::null_mut();
213            let stack = push(stack, Value::String(path.into()));
214            let stack = patch_seq_file_slurp(stack);
215
216            let (stack, value) = pop(stack);
217            match value {
218                Value::String(s) => assert_eq!(s.as_str(), ""),
219                _ => panic!("Expected String"),
220            }
221            assert!(stack.is_null());
222        }
223    }
224
225    #[test]
226    fn test_file_slurp_safe_success() {
227        let mut temp_file = NamedTempFile::new().unwrap();
228        writeln!(temp_file, "Safe read!").unwrap();
229        let path = temp_file.path().to_str().unwrap().to_string();
230
231        unsafe {
232            let stack = std::ptr::null_mut();
233            let stack = push(stack, Value::String(path.into()));
234            let stack = patch_seq_file_slurp_safe(stack);
235
236            let (stack, success) = pop(stack);
237            let (stack, contents) = pop(stack);
238            assert_eq!(success, Value::Int(1));
239            match contents {
240                Value::String(s) => assert_eq!(s.as_str().trim(), "Safe read!"),
241                _ => panic!("Expected String"),
242            }
243            assert!(stack.is_null());
244        }
245    }
246
247    #[test]
248    fn test_file_slurp_safe_not_found() {
249        unsafe {
250            let stack = std::ptr::null_mut();
251            let stack = push(stack, Value::String("/nonexistent/path/to/file.txt".into()));
252            let stack = patch_seq_file_slurp_safe(stack);
253
254            let (stack, success) = pop(stack);
255            let (stack, contents) = pop(stack);
256            assert_eq!(success, Value::Int(0));
257            match contents {
258                Value::String(s) => assert_eq!(s.as_str(), ""),
259                _ => panic!("Expected String"),
260            }
261            assert!(stack.is_null());
262        }
263    }
264
265    #[test]
266    fn test_file_slurp_safe_empty_file() {
267        let temp_file = NamedTempFile::new().unwrap();
268        let path = temp_file.path().to_str().unwrap().to_string();
269
270        unsafe {
271            let stack = std::ptr::null_mut();
272            let stack = push(stack, Value::String(path.into()));
273            let stack = patch_seq_file_slurp_safe(stack);
274
275            let (stack, success) = pop(stack);
276            let (stack, contents) = pop(stack);
277            assert_eq!(success, Value::Int(1)); // Empty file is still success
278            match contents {
279                Value::String(s) => assert_eq!(s.as_str(), ""),
280                _ => panic!("Expected String"),
281            }
282            assert!(stack.is_null());
283        }
284    }
285}