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}