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 {
499    use super::*;
500    use std::io::Write;
501    use tempfile::NamedTempFile;
502
503    #[test]
504    fn test_file_slurp() {
505        // Create a temporary file with known contents
506        let mut temp_file = NamedTempFile::new().unwrap();
507        writeln!(temp_file, "Hello, file!").unwrap();
508        let path = temp_file.path().to_str().unwrap().to_string();
509
510        unsafe {
511            let stack = crate::stack::alloc_test_stack();
512            let stack = push(stack, Value::String(path.into()));
513            let stack = patch_seq_file_slurp(stack);
514
515            // file-slurp now returns (contents Bool)
516            let (stack, success) = pop(stack);
517            assert_eq!(success, Value::Bool(true));
518            let (_stack, value) = pop(stack);
519            match value {
520                Value::String(s) => assert_eq!(s.as_str().trim(), "Hello, file!"),
521                _ => panic!("Expected String"),
522            }
523        }
524    }
525
526    #[test]
527    fn test_file_exists_true() {
528        let temp_file = NamedTempFile::new().unwrap();
529        let path = temp_file.path().to_str().unwrap().to_string();
530
531        unsafe {
532            let stack = crate::stack::alloc_test_stack();
533            let stack = push(stack, Value::String(path.into()));
534            let stack = patch_seq_file_exists(stack);
535
536            let (_stack, value) = pop(stack);
537            assert_eq!(value, Value::Bool(true));
538        }
539    }
540
541    #[test]
542    fn test_file_exists_false() {
543        unsafe {
544            let stack = crate::stack::alloc_test_stack();
545            let stack = push(stack, Value::String("/nonexistent/path/to/file.txt".into()));
546            let stack = patch_seq_file_exists(stack);
547
548            let (_stack, value) = pop(stack);
549            assert_eq!(value, Value::Bool(false));
550        }
551    }
552
553    #[test]
554    fn test_file_slurp_utf8() {
555        let mut temp_file = NamedTempFile::new().unwrap();
556        write!(temp_file, "Hello, δΈ–η•Œ! 🌍").unwrap();
557        let path = temp_file.path().to_str().unwrap().to_string();
558
559        unsafe {
560            let stack = crate::stack::alloc_test_stack();
561            let stack = push(stack, Value::String(path.into()));
562            let stack = patch_seq_file_slurp(stack);
563
564            // file-slurp returns (contents Bool)
565            let (stack, success) = pop(stack);
566            assert_eq!(success, Value::Bool(true));
567            let (_stack, value) = pop(stack);
568            match value {
569                Value::String(s) => assert_eq!(s.as_str(), "Hello, δΈ–η•Œ! 🌍"),
570                _ => panic!("Expected String"),
571            }
572        }
573    }
574
575    #[test]
576    fn test_file_slurp_empty() {
577        let temp_file = NamedTempFile::new().unwrap();
578        let path = temp_file.path().to_str().unwrap().to_string();
579
580        unsafe {
581            let stack = crate::stack::alloc_test_stack();
582            let stack = push(stack, Value::String(path.into()));
583            let stack = patch_seq_file_slurp(stack);
584
585            // file-slurp returns (contents Bool)
586            let (stack, success) = pop(stack);
587            assert_eq!(success, Value::Bool(true)); // Empty file is still success
588            let (_stack, value) = pop(stack);
589            match value {
590                Value::String(s) => assert_eq!(s.as_str(), ""),
591                _ => panic!("Expected String"),
592            }
593        }
594    }
595
596    #[test]
597    fn test_file_slurp_not_found() {
598        unsafe {
599            let stack = crate::stack::alloc_test_stack();
600            let stack = push(stack, Value::String("/nonexistent/path/to/file.txt".into()));
601            let stack = patch_seq_file_slurp(stack);
602
603            let (stack, success) = pop(stack);
604            let (_stack, contents) = pop(stack);
605            assert_eq!(success, Value::Bool(false));
606            match contents {
607                Value::String(s) => assert_eq!(s.as_str(), ""),
608                _ => panic!("Expected String"),
609            }
610        }
611    }
612
613    // ==========================================================================
614    // Tests for file.spit
615    // ==========================================================================
616
617    #[test]
618    fn test_file_spit_creates_new_file() {
619        let temp_dir = tempfile::tempdir().unwrap();
620        let path = temp_dir.path().join("test.txt");
621        let path_str = path.to_str().unwrap().to_string();
622
623        unsafe {
624            let stack = crate::stack::alloc_test_stack();
625            let stack = push(stack, Value::String("hello world".into()));
626            let stack = push(stack, Value::String(path_str.clone().into()));
627            let stack = patch_seq_file_spit(stack);
628
629            let (_stack, success) = pop(stack);
630            assert_eq!(success, Value::Bool(true));
631        }
632
633        // Verify file was created with correct contents
634        let contents = std::fs::read_to_string(&path).unwrap();
635        assert_eq!(contents, "hello world");
636    }
637
638    #[test]
639    fn test_file_spit_overwrites_existing() {
640        let mut temp_file = NamedTempFile::new().unwrap();
641        writeln!(temp_file, "old content").unwrap();
642        let path = temp_file.path().to_str().unwrap().to_string();
643
644        unsafe {
645            let stack = crate::stack::alloc_test_stack();
646            let stack = push(stack, Value::String("new content".into()));
647            let stack = push(stack, Value::String(path.clone().into()));
648            let stack = patch_seq_file_spit(stack);
649
650            let (_stack, success) = pop(stack);
651            assert_eq!(success, Value::Bool(true));
652        }
653
654        let contents = std::fs::read_to_string(&path).unwrap();
655        assert_eq!(contents, "new content");
656    }
657
658    #[test]
659    fn test_file_spit_invalid_path() {
660        unsafe {
661            let stack = crate::stack::alloc_test_stack();
662            let stack = push(stack, Value::String("content".into()));
663            let stack = push(stack, Value::String("/nonexistent/dir/file.txt".into()));
664            let stack = patch_seq_file_spit(stack);
665
666            let (_stack, success) = pop(stack);
667            assert_eq!(success, Value::Bool(false));
668        }
669    }
670
671    // ==========================================================================
672    // Tests for file.append
673    // ==========================================================================
674
675    #[test]
676    fn test_file_append_to_existing() {
677        let mut temp_file = NamedTempFile::new().unwrap();
678        write!(temp_file, "hello").unwrap();
679        let path = temp_file.path().to_str().unwrap().to_string();
680
681        unsafe {
682            let stack = crate::stack::alloc_test_stack();
683            let stack = push(stack, Value::String(" world".into()));
684            let stack = push(stack, Value::String(path.clone().into()));
685            let stack = patch_seq_file_append(stack);
686
687            let (_stack, success) = pop(stack);
688            assert_eq!(success, Value::Bool(true));
689        }
690
691        let contents = std::fs::read_to_string(&path).unwrap();
692        assert_eq!(contents, "hello world");
693    }
694
695    #[test]
696    fn test_file_append_creates_new() {
697        let temp_dir = tempfile::tempdir().unwrap();
698        let path = temp_dir.path().join("new.txt");
699        let path_str = path.to_str().unwrap().to_string();
700
701        unsafe {
702            let stack = crate::stack::alloc_test_stack();
703            let stack = push(stack, Value::String("content".into()));
704            let stack = push(stack, Value::String(path_str.clone().into()));
705            let stack = patch_seq_file_append(stack);
706
707            let (_stack, success) = pop(stack);
708            assert_eq!(success, Value::Bool(true));
709        }
710
711        let contents = std::fs::read_to_string(&path).unwrap();
712        assert_eq!(contents, "content");
713    }
714
715    // ==========================================================================
716    // Tests for file.delete
717    // ==========================================================================
718
719    #[test]
720    fn test_file_delete_existing() {
721        let temp_file = NamedTempFile::new().unwrap();
722        let path = temp_file.path().to_str().unwrap().to_string();
723        // Keep path but drop temp_file so we control the file
724        let path_copy = path.clone();
725        drop(temp_file);
726        std::fs::write(&path_copy, "content").unwrap();
727
728        unsafe {
729            let stack = crate::stack::alloc_test_stack();
730            let stack = push(stack, Value::String(path_copy.clone().into()));
731            let stack = patch_seq_file_delete(stack);
732
733            let (_stack, success) = pop(stack);
734            assert_eq!(success, Value::Bool(true));
735        }
736
737        assert!(!std::path::Path::new(&path_copy).exists());
738    }
739
740    #[test]
741    fn test_file_delete_nonexistent() {
742        unsafe {
743            let stack = crate::stack::alloc_test_stack();
744            let stack = push(stack, Value::String("/nonexistent/file.txt".into()));
745            let stack = patch_seq_file_delete(stack);
746
747            let (_stack, success) = pop(stack);
748            assert_eq!(success, Value::Bool(false));
749        }
750    }
751
752    // ==========================================================================
753    // Tests for file.size
754    // ==========================================================================
755
756    #[test]
757    fn test_file_size_existing() {
758        let mut temp_file = NamedTempFile::new().unwrap();
759        write!(temp_file, "hello world").unwrap(); // 11 bytes
760        let path = temp_file.path().to_str().unwrap().to_string();
761
762        unsafe {
763            let stack = crate::stack::alloc_test_stack();
764            let stack = push(stack, Value::String(path.into()));
765            let stack = patch_seq_file_size(stack);
766
767            let (stack, success) = pop(stack);
768            assert_eq!(success, Value::Bool(true));
769            let (_stack, size) = pop(stack);
770            assert_eq!(size, Value::Int(11));
771        }
772    }
773
774    #[test]
775    fn test_file_size_nonexistent() {
776        unsafe {
777            let stack = crate::stack::alloc_test_stack();
778            let stack = push(stack, Value::String("/nonexistent/file.txt".into()));
779            let stack = patch_seq_file_size(stack);
780
781            let (stack, success) = pop(stack);
782            assert_eq!(success, Value::Bool(false));
783            let (_stack, size) = pop(stack);
784            assert_eq!(size, Value::Int(0));
785        }
786    }
787
788    // ==========================================================================
789    // Tests for dir.exists?
790    // ==========================================================================
791
792    #[test]
793    fn test_dir_exists_true() {
794        let temp_dir = tempfile::tempdir().unwrap();
795        let path = temp_dir.path().to_str().unwrap().to_string();
796
797        unsafe {
798            let stack = crate::stack::alloc_test_stack();
799            let stack = push(stack, Value::String(path.into()));
800            let stack = patch_seq_dir_exists(stack);
801
802            let (_stack, exists) = pop(stack);
803            assert_eq!(exists, Value::Bool(true));
804        }
805    }
806
807    #[test]
808    fn test_dir_exists_false() {
809        unsafe {
810            let stack = crate::stack::alloc_test_stack();
811            let stack = push(stack, Value::String("/nonexistent/directory".into()));
812            let stack = patch_seq_dir_exists(stack);
813
814            let (_stack, exists) = pop(stack);
815            assert_eq!(exists, Value::Bool(false));
816        }
817    }
818
819    #[test]
820    fn test_dir_exists_file_is_not_dir() {
821        let temp_file = NamedTempFile::new().unwrap();
822        let path = temp_file.path().to_str().unwrap().to_string();
823
824        unsafe {
825            let stack = crate::stack::alloc_test_stack();
826            let stack = push(stack, Value::String(path.into()));
827            let stack = patch_seq_dir_exists(stack);
828
829            let (_stack, exists) = pop(stack);
830            assert_eq!(exists, Value::Bool(false)); // file is not a directory
831        }
832    }
833
834    // ==========================================================================
835    // Tests for dir.make
836    // ==========================================================================
837
838    #[test]
839    fn test_dir_make_success() {
840        let temp_dir = tempfile::tempdir().unwrap();
841        let new_dir = temp_dir.path().join("newdir");
842        let path = new_dir.to_str().unwrap().to_string();
843
844        unsafe {
845            let stack = crate::stack::alloc_test_stack();
846            let stack = push(stack, Value::String(path.clone().into()));
847            let stack = patch_seq_dir_make(stack);
848
849            let (_stack, success) = pop(stack);
850            assert_eq!(success, Value::Bool(true));
851        }
852
853        assert!(new_dir.is_dir());
854    }
855
856    #[test]
857    fn test_dir_make_nested() {
858        let temp_dir = tempfile::tempdir().unwrap();
859        let nested = temp_dir.path().join("a").join("b").join("c");
860        let path = nested.to_str().unwrap().to_string();
861
862        unsafe {
863            let stack = crate::stack::alloc_test_stack();
864            let stack = push(stack, Value::String(path.clone().into()));
865            let stack = patch_seq_dir_make(stack);
866
867            let (_stack, success) = pop(stack);
868            assert_eq!(success, Value::Bool(true));
869        }
870
871        assert!(nested.is_dir());
872    }
873
874    // ==========================================================================
875    // Tests for dir.delete
876    // ==========================================================================
877
878    #[test]
879    fn test_dir_delete_empty() {
880        let temp_dir = tempfile::tempdir().unwrap();
881        let to_delete = temp_dir.path().join("to_delete");
882        std::fs::create_dir(&to_delete).unwrap();
883        let path = to_delete.to_str().unwrap().to_string();
884
885        unsafe {
886            let stack = crate::stack::alloc_test_stack();
887            let stack = push(stack, Value::String(path.clone().into()));
888            let stack = patch_seq_dir_delete(stack);
889
890            let (_stack, success) = pop(stack);
891            assert_eq!(success, Value::Bool(true));
892        }
893
894        assert!(!to_delete.exists());
895    }
896
897    #[test]
898    fn test_dir_delete_nonempty_fails() {
899        let temp_dir = tempfile::tempdir().unwrap();
900        let to_delete = temp_dir.path().join("nonempty");
901        std::fs::create_dir(&to_delete).unwrap();
902        std::fs::write(to_delete.join("file.txt"), "content").unwrap();
903        let path = to_delete.to_str().unwrap().to_string();
904
905        unsafe {
906            let stack = crate::stack::alloc_test_stack();
907            let stack = push(stack, Value::String(path.clone().into()));
908            let stack = patch_seq_dir_delete(stack);
909
910            let (_stack, success) = pop(stack);
911            assert_eq!(success, Value::Bool(false)); // can't delete non-empty
912        }
913
914        assert!(to_delete.exists());
915    }
916
917    #[test]
918    fn test_dir_delete_nonexistent() {
919        unsafe {
920            let stack = crate::stack::alloc_test_stack();
921            let stack = push(stack, Value::String("/nonexistent/dir".into()));
922            let stack = patch_seq_dir_delete(stack);
923
924            let (_stack, success) = pop(stack);
925            assert_eq!(success, Value::Bool(false));
926        }
927    }
928
929    // ==========================================================================
930    // Tests for dir.list
931    // ==========================================================================
932
933    #[test]
934    fn test_dir_list_success() {
935        let temp_dir = tempfile::tempdir().unwrap();
936        std::fs::write(temp_dir.path().join("a.txt"), "a").unwrap();
937        std::fs::write(temp_dir.path().join("b.txt"), "b").unwrap();
938        let path = temp_dir.path().to_str().unwrap().to_string();
939
940        unsafe {
941            let stack = crate::stack::alloc_test_stack();
942            let stack = push(stack, Value::String(path.into()));
943            let stack = patch_seq_dir_list(stack);
944
945            let (stack, success) = pop(stack);
946            assert_eq!(success, Value::Bool(true));
947
948            let (_stack, list) = pop(stack);
949            match list {
950                Value::Variant(v) => {
951                    assert_eq!(v.tag.as_str(), "List");
952                    assert_eq!(v.fields.len(), 2);
953                }
954                _ => panic!("Expected Variant(List)"),
955            }
956        }
957    }
958
959    #[test]
960    fn test_dir_list_empty() {
961        let temp_dir = tempfile::tempdir().unwrap();
962        let path = temp_dir.path().to_str().unwrap().to_string();
963
964        unsafe {
965            let stack = crate::stack::alloc_test_stack();
966            let stack = push(stack, Value::String(path.into()));
967            let stack = patch_seq_dir_list(stack);
968
969            let (stack, success) = pop(stack);
970            assert_eq!(success, Value::Bool(true));
971
972            let (_stack, list) = pop(stack);
973            match list {
974                Value::Variant(v) => {
975                    assert_eq!(v.tag.as_str(), "List");
976                    assert_eq!(v.fields.len(), 0);
977                }
978                _ => panic!("Expected Variant(List)"),
979            }
980        }
981    }
982
983    #[test]
984    fn test_dir_list_nonexistent() {
985        unsafe {
986            let stack = crate::stack::alloc_test_stack();
987            let stack = push(stack, Value::String("/nonexistent/dir".into()));
988            let stack = patch_seq_dir_list(stack);
989
990            let (stack, success) = pop(stack);
991            assert_eq!(success, Value::Bool(false));
992
993            let (_stack, list) = pop(stack);
994            match list {
995                Value::Variant(v) => {
996                    assert_eq!(v.tag.as_str(), "List");
997                    assert_eq!(v.fields.len(), 0); // empty list on failure
998                }
999                _ => panic!("Expected Variant(List)"),
1000            }
1001        }
1002    }
1003}