Skip to main content

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//! "data.txt" [ process-line ] file-for-each-line+  # ( String Quotation -- String Int )
11//! ```
12//!
13//! # Example
14//!
15//! ```seq
16//! : main ( -- Int )
17//!   "config.json" file-exists? if
18//!     "config.json" file-slurp write_line
19//!   else
20//!     "File not found" write_line
21//!   then
22//!   0
23//! ;
24//! ```
25
26use crate::stack::{Stack, pop, push};
27use crate::value::{Value, VariantData};
28use std::fs::{self, File, OpenOptions};
29use std::io::{BufRead, BufReader, Write};
30use std::path::Path;
31use std::sync::Arc;
32
33/// Read entire file contents as a string
34///
35/// Stack effect: ( String -- String Bool )
36///
37/// Takes a file path, attempts to read the entire file.
38/// Returns (contents true) on success, or ("" false) on failure.
39/// Errors are values, not crashes.
40/// Panics only for internal bugs (wrong stack type).
41///
42/// # Safety
43/// - `stack` must be a valid, non-null stack pointer with a String value on top
44/// - Caller must ensure stack is not concurrently modified
45#[unsafe(no_mangle)]
46pub unsafe extern "C" fn patch_seq_file_slurp(stack: Stack) -> Stack {
47    assert!(!stack.is_null(), "file-slurp: stack is empty");
48
49    let (rest, value) = unsafe { pop(stack) };
50
51    match value {
52        Value::String(path) => match fs::read_to_string(path.as_str()) {
53            Ok(contents) => {
54                let stack = unsafe { push(rest, Value::String(contents.into())) };
55                unsafe { push(stack, Value::Bool(true)) }
56            }
57            Err(_) => {
58                let stack = unsafe { push(rest, Value::String("".into())) };
59                unsafe { push(stack, Value::Bool(false)) }
60            }
61        },
62        _ => panic!("file-slurp: expected String path on stack, got {:?}", value),
63    }
64}
65
66/// Check if a file exists
67///
68/// Stack effect: ( String -- Int )
69///
70/// Takes a file path and returns 1 if the file exists, 0 otherwise.
71///
72/// # Safety
73/// - `stack` must be a valid, non-null stack pointer with a String value on top
74/// - Caller must ensure stack is not concurrently modified
75#[unsafe(no_mangle)]
76pub unsafe extern "C" fn patch_seq_file_exists(stack: Stack) -> Stack {
77    assert!(!stack.is_null(), "file-exists?: stack is empty");
78
79    let (rest, value) = unsafe { pop(stack) };
80
81    match value {
82        Value::String(path) => {
83            let exists = Path::new(path.as_str()).exists();
84            unsafe { push(rest, Value::Bool(exists)) }
85        }
86        _ => panic!(
87            "file-exists?: expected String path on stack, got {:?}",
88            value
89        ),
90    }
91}
92
93/// Process each line of a file with a quotation
94///
95/// Stack effect: ( String Quotation -- String Int )
96///
97/// Opens the file, calls the quotation with each line (including newline),
98/// then closes the file.
99///
100/// Returns:
101/// - Success: ( "" 1 )
102/// - Error: ( "error message" 0 )
103///
104/// The quotation should have effect ( String -- ), receiving each line
105/// and consuming it. Empty files return success without calling the quotation.
106///
107/// # Line Ending Normalization
108///
109/// Line endings are normalized to `\n` regardless of platform. Windows-style
110/// `\r\n` endings are converted to `\n`. This ensures consistent behavior
111/// when processing files across different operating systems.
112///
113/// # Example
114///
115/// ```seq
116/// "data.txt" [ string-chomp process-line ] file-for-each-line+
117/// if
118///     "Done processing" write_line
119/// else
120///     "Error: " swap string-concat write_line
121/// then
122/// ```
123///
124/// # Safety
125/// - `stack` must be a valid, non-null stack pointer
126/// - Top of stack must be a Quotation or Closure
127/// - Second on stack must be a String (file path)
128#[unsafe(no_mangle)]
129pub unsafe extern "C" fn patch_seq_file_for_each_line_plus(stack: Stack) -> Stack {
130    assert!(!stack.is_null(), "file-for-each-line+: stack is empty");
131
132    // Pop quotation
133    let (stack, quot_value) = unsafe { pop(stack) };
134
135    // Pop path
136    let (stack, path_value) = unsafe { pop(stack) };
137    let path = match path_value {
138        Value::String(s) => s,
139        _ => panic!(
140            "file-for-each-line+: expected String path, got {:?}",
141            path_value
142        ),
143    };
144
145    // Open file
146    let file = match File::open(path.as_str()) {
147        Ok(f) => f,
148        Err(e) => {
149            // Return error: ( "error message" 0 )
150            let stack = unsafe { push(stack, Value::String(e.to_string().into())) };
151            return unsafe { push(stack, Value::Int(0)) };
152        }
153    };
154
155    // Extract function pointer and optionally closure environment
156    let (wrapper, env_data, env_len): (usize, *const Value, usize) = match quot_value {
157        Value::Quotation { wrapper, .. } => {
158            if wrapper == 0 {
159                panic!("file-for-each-line+: quotation wrapper function pointer is null");
160            }
161            (wrapper, std::ptr::null(), 0)
162        }
163        Value::Closure { fn_ptr, ref env } => {
164            if fn_ptr == 0 {
165                panic!("file-for-each-line+: closure function pointer is null");
166            }
167            (fn_ptr, env.as_ptr(), env.len())
168        }
169        _ => panic!(
170            "file-for-each-line+: expected Quotation or Closure, got {:?}",
171            quot_value
172        ),
173    };
174
175    // Read lines and call quotation/closure for each
176    let reader = BufReader::new(file);
177    let mut current_stack = stack;
178
179    for line_result in reader.lines() {
180        match line_result {
181            Ok(mut line_str) => {
182                // `BufReader::lines()` strips all line endings (\n, \r\n, \r)
183                // We add back \n to match read_line behavior and ensure consistent newlines
184                line_str.push('\n');
185
186                // Push line onto stack
187                current_stack = unsafe { push(current_stack, Value::String(line_str.into())) };
188
189                // Call the quotation or closure
190                if env_data.is_null() {
191                    // Quotation: just stack -> stack
192                    let fn_ref: unsafe extern "C" fn(Stack) -> Stack =
193                        unsafe { std::mem::transmute(wrapper) };
194                    current_stack = unsafe { fn_ref(current_stack) };
195                } else {
196                    // Closure: stack, env_ptr, env_len -> stack
197                    let fn_ref: unsafe extern "C" fn(Stack, *const Value, usize) -> Stack =
198                        unsafe { std::mem::transmute(wrapper) };
199                    current_stack = unsafe { fn_ref(current_stack, env_data, env_len) };
200                }
201
202                // Yield to scheduler for cooperative multitasking
203                may::coroutine::yield_now();
204            }
205            Err(e) => {
206                // I/O error mid-file
207                let stack = unsafe { push(current_stack, Value::String(e.to_string().into())) };
208                return unsafe { push(stack, Value::Bool(false)) };
209            }
210        }
211    }
212
213    // Success: ( "" true )
214    let stack = unsafe { push(current_stack, Value::String("".into())) };
215    unsafe { push(stack, Value::Bool(true)) }
216}
217
218/// Write string to file (creates or overwrites)
219///
220/// Stack effect: ( String String -- Bool )
221///
222/// Takes content and path, writes content to file.
223/// Creates the file if it doesn't exist, overwrites if it does.
224/// Returns true on success, false on failure.
225///
226/// # Safety
227/// - `stack` must be a valid, non-null stack pointer
228/// - Top of stack must be path (String), second must be content (String)
229#[unsafe(no_mangle)]
230pub unsafe extern "C" fn patch_seq_file_spit(stack: Stack) -> Stack {
231    assert!(!stack.is_null(), "file.spit: stack is empty");
232
233    // Pop path (top of stack)
234    let (stack, path_value) = unsafe { pop(stack) };
235    let path = match path_value {
236        Value::String(s) => s,
237        _ => panic!("file.spit: expected String path, got {:?}", path_value),
238    };
239
240    // Pop content
241    let (stack, content_value) = unsafe { pop(stack) };
242    let content = match content_value {
243        Value::String(s) => s,
244        _ => panic!(
245            "file.spit: expected String content, got {:?}",
246            content_value
247        ),
248    };
249
250    match fs::write(path.as_str(), content.as_str()) {
251        Ok(()) => unsafe { push(stack, Value::Bool(true)) },
252        Err(_) => unsafe { push(stack, Value::Bool(false)) },
253    }
254}
255
256/// Append string to file (creates if doesn't exist)
257///
258/// Stack effect: ( String String -- Bool )
259///
260/// Takes content and path, appends content to file.
261/// Creates the file if it doesn't exist.
262/// Returns true on success, false on failure.
263///
264/// # Safety
265/// - `stack` must be a valid, non-null stack pointer
266/// - Top of stack must be path (String), second must be content (String)
267#[unsafe(no_mangle)]
268pub unsafe extern "C" fn patch_seq_file_append(stack: Stack) -> Stack {
269    assert!(!stack.is_null(), "file.append: stack is empty");
270
271    // Pop path (top of stack)
272    let (stack, path_value) = unsafe { pop(stack) };
273    let path = match path_value {
274        Value::String(s) => s,
275        _ => panic!("file.append: expected String path, got {:?}", path_value),
276    };
277
278    // Pop content
279    let (stack, content_value) = unsafe { pop(stack) };
280    let content = match content_value {
281        Value::String(s) => s,
282        _ => panic!(
283            "file.append: expected String content, got {:?}",
284            content_value
285        ),
286    };
287
288    let result = OpenOptions::new()
289        .create(true)
290        .append(true)
291        .open(path.as_str())
292        .and_then(|mut file| file.write_all(content.as_str().as_bytes()));
293
294    match result {
295        Ok(()) => unsafe { push(stack, Value::Bool(true)) },
296        Err(_) => unsafe { push(stack, Value::Bool(false)) },
297    }
298}
299
300/// Delete a file
301///
302/// Stack effect: ( String -- Bool )
303///
304/// Takes a file path and deletes the file.
305/// Returns true on success, false on failure (including if file doesn't exist).
306///
307/// # Safety
308/// - `stack` must be a valid, non-null stack pointer
309/// - Top of stack must be path (String)
310#[unsafe(no_mangle)]
311pub unsafe extern "C" fn patch_seq_file_delete(stack: Stack) -> Stack {
312    assert!(!stack.is_null(), "file.delete: stack is empty");
313
314    let (stack, path_value) = unsafe { pop(stack) };
315    let path = match path_value {
316        Value::String(s) => s,
317        _ => panic!("file.delete: expected String path, got {:?}", path_value),
318    };
319
320    match fs::remove_file(path.as_str()) {
321        Ok(()) => unsafe { push(stack, Value::Bool(true)) },
322        Err(_) => unsafe { push(stack, Value::Bool(false)) },
323    }
324}
325
326/// Get file size in bytes
327///
328/// Stack effect: ( String -- Int Bool )
329///
330/// Takes a file path and returns (size, success).
331/// Returns (size, true) on success, (0, false) on failure.
332///
333/// # Safety
334/// - `stack` must be a valid, non-null stack pointer
335/// - Top of stack must be path (String)
336#[unsafe(no_mangle)]
337pub unsafe extern "C" fn patch_seq_file_size(stack: Stack) -> Stack {
338    assert!(!stack.is_null(), "file.size: stack is empty");
339
340    let (stack, path_value) = unsafe { pop(stack) };
341    let path = match path_value {
342        Value::String(s) => s,
343        _ => panic!("file.size: expected String path, got {:?}", path_value),
344    };
345
346    match fs::metadata(path.as_str()) {
347        Ok(metadata) => {
348            let size = metadata.len() as i64;
349            let stack = unsafe { push(stack, Value::Int(size)) };
350            unsafe { push(stack, Value::Bool(true)) }
351        }
352        Err(_) => {
353            let stack = unsafe { push(stack, Value::Int(0)) };
354            unsafe { push(stack, Value::Bool(false)) }
355        }
356    }
357}
358
359// =============================================================================
360// Directory Operations
361// =============================================================================
362
363/// Check if a directory exists
364///
365/// Stack effect: ( String -- Bool )
366///
367/// Takes a path and returns true if it exists and is a directory.
368///
369/// # Safety
370/// - `stack` must be a valid, non-null stack pointer
371/// - Top of stack must be path (String)
372#[unsafe(no_mangle)]
373pub unsafe extern "C" fn patch_seq_dir_exists(stack: Stack) -> Stack {
374    assert!(!stack.is_null(), "dir.exists?: stack is empty");
375
376    let (stack, path_value) = unsafe { pop(stack) };
377    let path = match path_value {
378        Value::String(s) => s,
379        _ => panic!("dir.exists?: expected String path, got {:?}", path_value),
380    };
381
382    let exists = Path::new(path.as_str()).is_dir();
383    unsafe { push(stack, Value::Bool(exists)) }
384}
385
386/// Create a directory (and parent directories if needed)
387///
388/// Stack effect: ( String -- Bool )
389///
390/// Takes a path and creates the directory and any missing parent directories.
391/// Returns true on success, false on failure.
392///
393/// # Safety
394/// - `stack` must be a valid, non-null stack pointer
395/// - Top of stack must be path (String)
396#[unsafe(no_mangle)]
397pub unsafe extern "C" fn patch_seq_dir_make(stack: Stack) -> Stack {
398    assert!(!stack.is_null(), "dir.make: stack is empty");
399
400    let (stack, path_value) = unsafe { pop(stack) };
401    let path = match path_value {
402        Value::String(s) => s,
403        _ => panic!("dir.make: expected String path, got {:?}", path_value),
404    };
405
406    match fs::create_dir_all(path.as_str()) {
407        Ok(()) => unsafe { push(stack, Value::Bool(true)) },
408        Err(_) => unsafe { push(stack, Value::Bool(false)) },
409    }
410}
411
412/// Delete an empty directory
413///
414/// Stack effect: ( String -- Bool )
415///
416/// Takes a path and deletes the directory (must be empty).
417/// Returns true on success, false on failure.
418///
419/// # Safety
420/// - `stack` must be a valid, non-null stack pointer
421/// - Top of stack must be path (String)
422#[unsafe(no_mangle)]
423pub unsafe extern "C" fn patch_seq_dir_delete(stack: Stack) -> Stack {
424    assert!(!stack.is_null(), "dir.delete: stack is empty");
425
426    let (stack, path_value) = unsafe { pop(stack) };
427    let path = match path_value {
428        Value::String(s) => s,
429        _ => panic!("dir.delete: expected String path, got {:?}", path_value),
430    };
431
432    match fs::remove_dir(path.as_str()) {
433        Ok(()) => unsafe { push(stack, Value::Bool(true)) },
434        Err(_) => unsafe { push(stack, Value::Bool(false)) },
435    }
436}
437
438/// List directory contents
439///
440/// Stack effect: ( String -- List Bool )
441///
442/// Takes a directory path and returns (list-of-names, success).
443/// Returns a list of filenames (strings) on success.
444///
445/// # Safety
446/// - `stack` must be a valid, non-null stack pointer
447/// - Top of stack must be path (String)
448#[unsafe(no_mangle)]
449pub unsafe extern "C" fn patch_seq_dir_list(stack: Stack) -> Stack {
450    assert!(!stack.is_null(), "dir.list: stack is empty");
451
452    let (stack, path_value) = unsafe { pop(stack) };
453    let path = match path_value {
454        Value::String(s) => s,
455        _ => panic!("dir.list: expected String path, got {:?}", path_value),
456    };
457
458    match fs::read_dir(path.as_str()) {
459        Ok(entries) => {
460            let mut names: Vec<Value> = Vec::new();
461            for entry in entries.flatten() {
462                if let Some(name) = entry.file_name().to_str() {
463                    names.push(Value::String(name.to_string().into()));
464                }
465            }
466            let list = Value::Variant(Arc::new(VariantData::new(
467                crate::seqstring::global_string("List".to_string()),
468                names,
469            )));
470            let stack = unsafe { push(stack, list) };
471            unsafe { push(stack, Value::Bool(true)) }
472        }
473        Err(_) => {
474            let empty_list = Value::Variant(Arc::new(VariantData::new(
475                crate::seqstring::global_string("List".to_string()),
476                vec![],
477            )));
478            let stack = unsafe { push(stack, empty_list) };
479            unsafe { push(stack, Value::Bool(false)) }
480        }
481    }
482}
483
484// Public re-exports
485pub use patch_seq_dir_delete as dir_delete;
486pub use patch_seq_dir_exists as dir_exists;
487pub use patch_seq_dir_list as dir_list;
488pub use patch_seq_dir_make as dir_make;
489pub use patch_seq_file_append as file_append;
490pub use patch_seq_file_delete as file_delete;
491pub use patch_seq_file_exists as file_exists;
492pub use patch_seq_file_for_each_line_plus as file_for_each_line_plus;
493pub use patch_seq_file_size as file_size;
494pub use patch_seq_file_slurp as file_slurp;
495pub use patch_seq_file_spit as file_spit;
496
497#[cfg(test)]
498mod tests;