Skip to main content

seq_runtime/
os.rs

1//! OS operations for Seq
2//!
3//! Provides portable OS interaction primitives: environment variables,
4//! paths, and system information.
5//!
6//! These functions are exported with C ABI for LLVM codegen to call.
7
8use crate::seqstring::global_string;
9use crate::stack::{Stack, pop, push};
10use crate::value::Value;
11
12/// Path conversion idiom: paths are inherently text on the OS APIs we
13/// target (Linux/macOS POSIX, which expose `&str` via Rust's `Path`),
14/// so non-UTF-8 path bytes can't be handed to the OS as-is.
15/// `SeqString::as_str_or_empty()` returns `""` for non-UTF-8 input,
16/// which routes the call through the OS error path and produces the
17/// standard `(empty, false)` failure tuple — same observable result
18/// as if we'd validated upfront. Mirrors `file::path_str` for parity.
19fn path_str(s: &crate::seqstring::SeqString) -> &str {
20    s.as_str_or_empty()
21}
22
23/// Get an environment variable
24///
25/// Stack effect: ( name -- value success )
26///
27/// Returns the value and 1 on success, "" and 0 on failure.
28///
29/// # Safety
30/// Stack must have a String (variable name) on top
31#[unsafe(no_mangle)]
32pub unsafe extern "C" fn patch_seq_getenv(stack: Stack) -> Stack {
33    unsafe {
34        let (stack, name_val) = pop(stack);
35        let name = match name_val {
36            Value::String(s) => s,
37            _ => panic!(
38                "getenv: expected String (name) on stack, got {:?}",
39                name_val
40            ),
41        };
42
43        match std::env::var(name.as_str_or_empty()) {
44            Ok(value) => {
45                let stack = push(stack, Value::String(global_string(value)));
46                push(stack, Value::Bool(true)) // success
47            }
48            Err(_) => {
49                let stack = push(stack, Value::String(global_string(String::new())));
50                push(stack, Value::Bool(false)) // failure
51            }
52        }
53    }
54}
55
56/// Get the user's home directory
57///
58/// Stack effect: ( -- path success )
59///
60/// Returns the path and 1 on success, "" and 0 on failure.
61///
62/// # Safety
63/// Stack pointer must be valid
64#[unsafe(no_mangle)]
65pub unsafe extern "C" fn patch_seq_home_dir(stack: Stack) -> Stack {
66    unsafe {
67        // Try HOME env var first (works on Unix and some Windows configs)
68        if let Ok(home) = std::env::var("HOME") {
69            let stack = push(stack, Value::String(global_string(home)));
70            return push(stack, Value::Bool(true));
71        }
72
73        // On Windows, try USERPROFILE
74        #[cfg(windows)]
75        if let Ok(home) = std::env::var("USERPROFILE") {
76            let stack = push(stack, Value::String(global_string(home)));
77            return push(stack, Value::Bool(true));
78        }
79
80        // Fallback: return empty string with failure flag
81        let stack = push(stack, Value::String(global_string(String::new())));
82        push(stack, Value::Bool(false))
83    }
84}
85
86/// Get the current working directory
87///
88/// Stack effect: ( -- path success )
89///
90/// Returns the path and 1 on success, "" and 0 on failure.
91///
92/// # Safety
93/// Stack pointer must be valid
94#[unsafe(no_mangle)]
95pub unsafe extern "C" fn patch_seq_current_dir(stack: Stack) -> Stack {
96    unsafe {
97        match std::env::current_dir() {
98            Ok(path) => {
99                let path_str = path.to_string_lossy().into_owned();
100                let stack = push(stack, Value::String(global_string(path_str)));
101                push(stack, Value::Bool(true)) // success
102            }
103            Err(_) => {
104                let stack = push(stack, Value::String(global_string(String::new())));
105                push(stack, Value::Bool(false)) // failure
106            }
107        }
108    }
109}
110
111/// Check if a path exists
112///
113/// Stack effect: ( path -- exists )
114///
115/// Returns 1 if path exists, 0 otherwise.
116///
117/// # Safety
118/// Stack must have a String (path) on top
119#[unsafe(no_mangle)]
120pub unsafe extern "C" fn patch_seq_path_exists(stack: Stack) -> Stack {
121    unsafe {
122        let (stack, path_val) = pop(stack);
123        let path = match path_val {
124            Value::String(s) => s,
125            _ => panic!(
126                "path-exists: expected String (path) on stack, got {:?}",
127                path_val
128            ),
129        };
130
131        let exists = std::path::Path::new(path_str(&path)).exists();
132        push(stack, Value::Bool(exists))
133    }
134}
135
136/// Check if a path is a regular file
137///
138/// Stack effect: ( path -- is-file )
139///
140/// Returns 1 if path is a regular file, 0 otherwise.
141///
142/// # Safety
143/// Stack must have a String (path) on top
144#[unsafe(no_mangle)]
145pub unsafe extern "C" fn patch_seq_path_is_file(stack: Stack) -> Stack {
146    unsafe {
147        let (stack, path_val) = pop(stack);
148        let path = match path_val {
149            Value::String(s) => s,
150            _ => panic!(
151                "path-is-file: expected String (path) on stack, got {:?}",
152                path_val
153            ),
154        };
155
156        let is_file = std::path::Path::new(path_str(&path)).is_file();
157        push(stack, Value::Bool(is_file))
158    }
159}
160
161/// Check if a path is a directory
162///
163/// Stack effect: ( path -- is-dir )
164///
165/// Returns 1 if path is a directory, 0 otherwise.
166///
167/// # Safety
168/// Stack must have a String (path) on top
169#[unsafe(no_mangle)]
170pub unsafe extern "C" fn patch_seq_path_is_dir(stack: Stack) -> Stack {
171    unsafe {
172        let (stack, path_val) = pop(stack);
173        let path = match path_val {
174            Value::String(s) => s,
175            _ => panic!(
176                "path-is-dir: expected String (path) on stack, got {:?}",
177                path_val
178            ),
179        };
180
181        let is_dir = std::path::Path::new(path_str(&path)).is_dir();
182        push(stack, Value::Bool(is_dir))
183    }
184}
185
186/// Join two path components
187///
188/// Stack effect: ( base component -- joined )
189///
190/// Joins the base path with the component using the platform's path separator.
191///
192/// # Safety
193/// Stack must have two Strings on top (base, then component)
194#[unsafe(no_mangle)]
195pub unsafe extern "C" fn patch_seq_path_join(stack: Stack) -> Stack {
196    unsafe {
197        let (stack, component_val) = pop(stack);
198        let (stack, base_val) = pop(stack);
199
200        let base = match base_val {
201            Value::String(s) => s,
202            _ => panic!(
203                "path-join: expected String (base) on stack, got {:?}",
204                base_val
205            ),
206        };
207
208        let component = match component_val {
209            Value::String(s) => s,
210            _ => panic!(
211                "path-join: expected String (component) on stack, got {:?}",
212                component_val
213            ),
214        };
215
216        let joined = std::path::Path::new(path_str(&base))
217            .join(path_str(&component))
218            .to_string_lossy()
219            .into_owned();
220
221        push(stack, Value::String(global_string(joined)))
222    }
223}
224
225/// Get the parent directory of a path
226///
227/// Stack effect: ( path -- parent success )
228///
229/// Returns the parent directory and true on success, "" and false if no parent.
230///
231/// # Safety
232/// Stack must have a String (path) on top
233#[unsafe(no_mangle)]
234pub unsafe extern "C" fn patch_seq_path_parent(stack: Stack) -> Stack {
235    unsafe {
236        let (stack, path_val) = pop(stack);
237        let path = match path_val {
238            Value::String(s) => s,
239            _ => panic!(
240                "path-parent: expected String (path) on stack, got {:?}",
241                path_val
242            ),
243        };
244
245        match std::path::Path::new(path_str(&path)).parent() {
246            Some(parent) => {
247                let parent_str = parent.to_string_lossy().into_owned();
248                let stack = push(stack, Value::String(global_string(parent_str)));
249                push(stack, Value::Bool(true)) // success
250            }
251            None => {
252                let stack = push(stack, Value::String(global_string(String::new())));
253                push(stack, Value::Bool(false)) // no parent
254            }
255        }
256    }
257}
258
259/// Get the filename component of a path
260///
261/// Stack effect: ( path -- filename success )
262///
263/// Returns the filename and true on success, "" and false if no filename.
264///
265/// # Safety
266/// Stack must have a String (path) on top
267#[unsafe(no_mangle)]
268pub unsafe extern "C" fn patch_seq_path_filename(stack: Stack) -> Stack {
269    unsafe {
270        let (stack, path_val) = pop(stack);
271        let path = match path_val {
272            Value::String(s) => s,
273            _ => panic!(
274                "path-filename: expected String (path) on stack, got {:?}",
275                path_val
276            ),
277        };
278
279        match std::path::Path::new(path_str(&path)).file_name() {
280            Some(filename) => {
281                let filename_str = filename.to_string_lossy().into_owned();
282                let stack = push(stack, Value::String(global_string(filename_str)));
283                push(stack, Value::Bool(true)) // success
284            }
285            None => {
286                let stack = push(stack, Value::String(global_string(String::new())));
287                push(stack, Value::Bool(false)) // no filename
288            }
289        }
290    }
291}
292
293/// Valid exit code range for Unix compatibility (only low 8 bits are meaningful)
294const EXIT_CODE_MIN: i64 = 0;
295const EXIT_CODE_MAX: i64 = 255;
296
297/// Exit the process with the given exit code
298///
299/// Stack effect: ( code -- )
300///
301/// Exit code must be in range 0-255 for Unix compatibility.
302/// This function does not return.
303///
304/// # Safety
305/// Stack must have an Int (exit code) on top.
306///
307/// Note: Returns `Stack` for LLVM ABI compatibility even though it never returns.
308#[unsafe(no_mangle)]
309pub unsafe extern "C" fn patch_seq_exit(stack: Stack) -> Stack {
310    unsafe {
311        let (_stack, code_val) = pop(stack);
312        let code = match code_val {
313            Value::Int(n) => {
314                if !(EXIT_CODE_MIN..=EXIT_CODE_MAX).contains(&n) {
315                    panic!(
316                        "os.exit: exit code must be in range {}-{}, got {}",
317                        EXIT_CODE_MIN, EXIT_CODE_MAX, n
318                    );
319                }
320                n as i32
321            }
322            _ => panic!(
323                "os.exit: expected Int (exit code) on stack, got {:?}",
324                code_val
325            ),
326        };
327
328        std::process::exit(code);
329    }
330}
331
332/// Get the operating system name
333///
334/// Stack effect: ( -- name )
335///
336/// Returns one of: "darwin", "linux", "windows", "freebsd", "openbsd", "netbsd",
337/// or "unknown" for unrecognized platforms.
338///
339/// # Safety
340/// Stack pointer must be valid
341#[unsafe(no_mangle)]
342pub unsafe extern "C" fn patch_seq_os_name(stack: Stack) -> Stack {
343    let name = if cfg!(target_os = "macos") {
344        "darwin"
345    } else if cfg!(target_os = "linux") {
346        "linux"
347    } else if cfg!(target_os = "windows") {
348        "windows"
349    } else if cfg!(target_os = "freebsd") {
350        "freebsd"
351    } else if cfg!(target_os = "openbsd") {
352        "openbsd"
353    } else if cfg!(target_os = "netbsd") {
354        "netbsd"
355    } else {
356        "unknown"
357    };
358
359    unsafe { push(stack, Value::String(global_string(name.to_owned()))) }
360}
361
362/// Get the CPU architecture
363///
364/// Stack effect: ( -- arch )
365///
366/// Returns one of: "x86_64", "aarch64", "arm", "x86", "riscv64",
367/// or "unknown" for unrecognized architectures.
368///
369/// # Safety
370/// Stack pointer must be valid
371#[unsafe(no_mangle)]
372pub unsafe extern "C" fn patch_seq_os_arch(stack: Stack) -> Stack {
373    let arch = if cfg!(target_arch = "x86_64") {
374        "x86_64"
375    } else if cfg!(target_arch = "aarch64") {
376        "aarch64"
377    } else if cfg!(target_arch = "arm") {
378        "arm"
379    } else if cfg!(target_arch = "x86") {
380        "x86"
381    } else if cfg!(target_arch = "riscv64") {
382        "riscv64"
383    } else {
384        "unknown"
385    };
386
387    unsafe { push(stack, Value::String(global_string(arch.to_owned()))) }
388}
389
390#[cfg(test)]
391mod tests;