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// Public re-exports
91pub use patch_seq_file_exists as file_exists;
92pub use patch_seq_file_slurp as file_slurp;
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use std::io::Write;
98    use tempfile::NamedTempFile;
99
100    #[test]
101    fn test_file_slurp() {
102        // Create a temporary file with known contents
103        let mut temp_file = NamedTempFile::new().unwrap();
104        writeln!(temp_file, "Hello, file!").unwrap();
105        let path = temp_file.path().to_str().unwrap().to_string();
106
107        unsafe {
108            let stack = std::ptr::null_mut();
109            let stack = push(stack, Value::String(path.into()));
110            let stack = patch_seq_file_slurp(stack);
111
112            let (stack, value) = pop(stack);
113            match value {
114                Value::String(s) => assert_eq!(s.as_str().trim(), "Hello, file!"),
115                _ => panic!("Expected String"),
116            }
117            assert!(stack.is_null());
118        }
119    }
120
121    #[test]
122    fn test_file_exists_true() {
123        let temp_file = NamedTempFile::new().unwrap();
124        let path = temp_file.path().to_str().unwrap().to_string();
125
126        unsafe {
127            let stack = std::ptr::null_mut();
128            let stack = push(stack, Value::String(path.into()));
129            let stack = patch_seq_file_exists(stack);
130
131            let (stack, value) = pop(stack);
132            assert_eq!(value, Value::Int(1));
133            assert!(stack.is_null());
134        }
135    }
136
137    #[test]
138    fn test_file_exists_false() {
139        unsafe {
140            let stack = std::ptr::null_mut();
141            let stack = push(stack, Value::String("/nonexistent/path/to/file.txt".into()));
142            let stack = patch_seq_file_exists(stack);
143
144            let (stack, value) = pop(stack);
145            assert_eq!(value, Value::Int(0));
146            assert!(stack.is_null());
147        }
148    }
149
150    #[test]
151    fn test_file_slurp_utf8() {
152        let mut temp_file = NamedTempFile::new().unwrap();
153        write!(temp_file, "Hello, δΈ–η•Œ! 🌍").unwrap();
154        let path = temp_file.path().to_str().unwrap().to_string();
155
156        unsafe {
157            let stack = std::ptr::null_mut();
158            let stack = push(stack, Value::String(path.into()));
159            let stack = patch_seq_file_slurp(stack);
160
161            let (stack, value) = pop(stack);
162            match value {
163                Value::String(s) => assert_eq!(s.as_str(), "Hello, δΈ–η•Œ! 🌍"),
164                _ => panic!("Expected String"),
165            }
166            assert!(stack.is_null());
167        }
168    }
169
170    #[test]
171    fn test_file_slurp_empty() {
172        let temp_file = NamedTempFile::new().unwrap();
173        let path = temp_file.path().to_str().unwrap().to_string();
174
175        unsafe {
176            let stack = std::ptr::null_mut();
177            let stack = push(stack, Value::String(path.into()));
178            let stack = patch_seq_file_slurp(stack);
179
180            let (stack, value) = pop(stack);
181            match value {
182                Value::String(s) => assert_eq!(s.as_str(), ""),
183                _ => panic!("Expected String"),
184            }
185            assert!(stack.is_null());
186        }
187    }
188}