seq_runtime/
args.rs

1//! Command-line argument handling for Seq
2//!
3//! Provides access to command-line arguments passed to the program.
4//!
5//! # Usage from Seq
6//!
7//! ```seq
8//! arg-count  # ( -- Int ) number of arguments (including program name)
9//! 0 arg      # ( Int -- String ) get argument at index
10//! ```
11//!
12//! # Example
13//!
14//! ```seq
15//! : main ( -- Int )
16//!   arg-count 1 > if
17//!     1 arg write_line  # Print first argument (after program name)
18//!   else
19//!     "No arguments provided" write_line
20//!   then
21//!   0
22//! ;
23//! ```
24
25use crate::stack::{Stack, push};
26use crate::value::Value;
27use std::ffi::CStr;
28use std::sync::OnceLock;
29
30/// Global storage for command-line arguments
31static ARGS: OnceLock<Vec<String>> = OnceLock::new();
32
33/// Initialize command-line arguments from C-style argc/argv
34///
35/// Called once at program startup from main() before any Seq code runs.
36///
37/// # Safety
38/// - argc must accurately reflect the number of pointers in argv
39/// - argv must contain argc valid, null-terminated C strings
40/// - argv pointers must remain valid for the duration of this call
41#[unsafe(no_mangle)]
42pub unsafe extern "C" fn patch_seq_args_init(argc: i32, argv: *const *const i8) {
43    let args: Vec<String> = (0..argc)
44        .map(|i| {
45            let ptr = unsafe { *argv.offset(i as isize) };
46            if ptr.is_null() {
47                String::new()
48            } else {
49                unsafe { CStr::from_ptr(ptr).to_str().unwrap_or("").to_owned() }
50            }
51        })
52        .collect();
53
54    // Set once - ignore if already set (shouldn't happen in normal use)
55    let _ = ARGS.set(args);
56}
57
58/// Get the number of command-line arguments
59///
60/// Stack effect: ( -- Int )
61///
62/// Returns the total count including the program name (argv[0]).
63/// A program run with no arguments returns 1.
64///
65/// # Safety
66/// - `stack` must be a valid stack pointer (may be null for empty stack)
67/// - Caller must ensure stack is not concurrently modified
68#[unsafe(no_mangle)]
69pub unsafe extern "C" fn patch_seq_arg_count(stack: Stack) -> Stack {
70    let count = ARGS.get().map(|a| a.len()).unwrap_or(0) as i64;
71    unsafe { push(stack, Value::Int(count)) }
72}
73
74/// Get command-line argument at index
75///
76/// Stack effect: ( Int -- String )
77///
78/// Index 0 is the program name. Returns empty string if index is out of bounds.
79///
80/// # Safety
81/// - `stack` must be a valid, non-null stack pointer with at least one Int value
82/// - Caller must ensure stack is not concurrently modified
83#[unsafe(no_mangle)]
84pub unsafe extern "C" fn patch_seq_arg_at(stack: Stack) -> Stack {
85    use crate::stack::pop;
86
87    assert!(!stack.is_null(), "arg: stack is empty");
88
89    let (rest, value) = unsafe { pop(stack) };
90
91    match value {
92        Value::Int(idx) => {
93            // Validate index is non-negative
94            if idx < 0 {
95                panic!("arg: index must be non-negative, got {}", idx);
96            }
97
98            let arg = ARGS
99                .get()
100                .and_then(|args| args.get(idx as usize))
101                .cloned()
102                .unwrap_or_default();
103
104            unsafe { push(rest, Value::String(arg.into())) }
105        }
106        _ => panic!("arg: expected Int index on stack, got {:?}", value),
107    }
108}
109
110// Public re-exports
111pub use patch_seq_arg_at as arg_at;
112pub use patch_seq_arg_count as arg_count;
113pub use patch_seq_args_init as args_init;
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::stack::pop;
119    use std::ptr;
120
121    #[test]
122    fn test_arg_count_no_init() {
123        // Before init, should return 0
124        // Note: Can't really test this in isolation since OnceLock is global
125        // This test mainly verifies the function doesn't crash
126        unsafe {
127            let stack = ptr::null_mut();
128            let stack = patch_seq_arg_count(stack);
129            let (_, value) = pop(stack);
130            // Could be 0 or whatever was set by previous test
131            assert!(matches!(value, Value::Int(_)));
132        }
133    }
134}