Skip to main content

catallaxy_stdlib/
lib.rs

1//! Native implementations of the Catallaxy standard library.
2//!
3//! Each native receives the calling thread's [`Vm`] and its arguments (the receiver first for
4//! methods). A missing optional argument arrives as the empty marker; use [`opt`].
5
6mod coll;
7mod conc;
8pub use conc::report_unjoined_panics;
9mod core;
10mod data;
11pub mod fiber;
12mod net;
13mod script;
14mod sys;
15
16use std::io::Write;
17use std::sync::OnceLock;
18
19use catallaxy_runtime::class::class_of;
20use catallaxy_runtime::object::str_of;
21use catallaxy_runtime::ops;
22use catallaxy_runtime::ownership;
23use catallaxy_runtime::{panic, try_push, try_vec, RtResult, Trap, Value};
24use catallaxy_vm::{NativeFn, Vm};
25use parking_lot::Mutex;
26
27/// Command-line arguments passed to the program (after the script path).
28pub static ARGS: OnceLock<Vec<String>> = OnceLock::new();
29
30static STDOUT: Mutex<Option<std::io::BufWriter<std::io::Stdout>>> = Mutex::new(None);
31
32/// Writes to the buffered standard output.
33pub fn write_out(s: &str) {
34    let mut g = STDOUT.lock();
35    let w = g.get_or_insert_with(|| std::io::BufWriter::with_capacity(1 << 16, std::io::stdout()));
36    let _ = w.write_all(s.as_bytes());
37}
38
39/// Flushes buffered standard output (at exit, before reading input, before errors).
40pub fn flush() {
41    if let Some(w) = STDOUT.lock().as_mut() {
42        let _ = w.flush();
43    }
44}
45
46/// The Catallaxy panic message for a Rust panic caught at a thread's or fiber's entry (an
47/// internal error of the runtime), or `None` if the payload is not a panic message (such as the
48/// forced unwind that drops a suspended fiber), which the catcher must resume.
49pub fn internal_error_message(payload: &(dyn std::any::Any + Send)) -> Option<String> {
50    let msg = if let Some(s) = payload.downcast_ref::<&'static str>() {
51        (*s).to_string()
52    } else {
53        payload.downcast_ref::<String>()?.clone()
54    };
55    Some(format!("internal error: {msg}"))
56}
57
58/// Appends up to eight frames of `trace` to a panic message, as a thread's panic report does.
59fn with_trace(mut msg: String, trace: &[String]) -> String {
60    for line in trace.iter().take(8) {
61        msg.push_str(&format!("\n    at {line}"));
62    }
63    msg
64}
65
66static EXTENSIONS: std::sync::LazyLock<parking_lot::RwLock<rustc_hash::FxHashMap<String, NativeFn>>> =
67    std::sync::LazyLock::new(Default::default);
68
69/// Registers a native function implemented by a Rust extension crate. Catallaxy code declares it
70/// with `native fn name(...) -> T` (a top-level `native fn` uses its plain name; a `native fn` in
71/// a class `C` uses `C.name`). Must be called before the program is loaded.
72pub fn register_native(name: &str, f: NativeFn) {
73    EXTENSIONS.write().insert(name.to_string(), f);
74}
75
76/// Looks up a native implementation by name: the standard library, then extensions.
77pub fn lookup(name: &str) -> Option<NativeFn> {
78    // Natives are looked up while the program loads: start the monotonic clock there, so
79    // `Time.monotonic` and `Instant` count from the start of the program.
80    sys::init_clock();
81    core::lookup(name)
82        .or_else(|| coll::lookup(name))
83        .or_else(|| conc::lookup(name))
84        .or_else(|| sys::lookup(name))
85        .or_else(|| script::lookup(name))
86        .or_else(|| net::lookup(name))
87        .or_else(|| data::lookup(name))
88        .or_else(|| EXTENSIONS.read().get(name).copied())
89}
90
91// ---- argument helpers ------------------------------------------------------------------------
92
93pub(crate) fn opt(v: Value) -> Option<Value> {
94    if v.is_empty_marker() {
95        None
96    } else {
97        Some(v)
98    }
99}
100
101pub(crate) fn arg(args: &[Value], i: usize) -> Option<Value> {
102    args.get(i).copied().and_then(opt)
103}
104
105pub(crate) fn s<'a>(v: Value) -> RtResult<&'a str> {
106    match str_of(v) {
107        Some(s) => Ok(s),
108        None => panic(format!("TypeError: expected String, got {}", class_of(v).name)),
109    }
110}
111
112/// The string object behind `v`.
113pub(crate) fn str_obj<'a>(v: Value) -> RtResult<&'a catallaxy_runtime::object::StrObj> {
114    catallaxy_runtime::object::str_obj_of(v)
115}
116
117pub(crate) fn int(v: Value) -> RtResult<i64> {
118    if v.is_small_int() {
119        return Ok(v.as_small_int());
120    }
121    if ops::is_int(v) {
122        return match ops::int_as_i64(v) {
123            Some(i) => Ok(i),
124            None => panic("RangeError: integer too large"),
125        };
126    }
127    panic(format!("TypeError: expected Int, got {}", class_of(v).name))
128}
129
130/// `v` itself if it is an Int (small or big), else a `TypeError` panic.
131pub(crate) fn int_value(v: Value) -> RtResult<Value> {
132    if ops::is_int(v) {
133        Ok(v)
134    } else {
135        catallaxy_runtime::object::type_error("Int", v)
136    }
137}
138
139/// `v` itself if it is a Bool, else a `TypeError` panic.
140pub(crate) fn bool_value(v: Value) -> RtResult<Value> {
141    if v.is_bool() {
142        Ok(v)
143    } else {
144        catallaxy_runtime::object::type_error("Bool", v)
145    }
146}
147
148pub(crate) fn float(v: Value) -> RtResult<f64> {
149    if v.is_float() {
150        Ok(v.as_f64())
151    } else if ops::is_int(v) {
152        Ok(ops::int_to_f64(v))
153    } else {
154        panic(format!("TypeError: expected Float, got {}", class_of(v).name))
155    }
156}
157
158pub(crate) fn new_str(vm: &mut Vm, s: &str) -> Value {
159    vm.m.alloc_str(s)
160}
161
162/// A list holding `items` (taking over the vector), or a `MemoryError` panic if its buffer
163/// cannot be had. Nothing roots `items`, so there is no collection and retry.
164pub(crate) fn new_list(vm: &mut Vm, items: Vec<Value>) -> RtResult<Value> {
165    vm.m.try_alloc_list(items)
166}
167
168/// A list of new strings, one per part, or a `MemoryError` panic. The parts may point into a
169/// rooted string: allocation never moves objects, and nothing here collects. A native that
170/// holds no unrooted values should call `vm.m.collect_if_refused()` first.
171pub(crate) fn new_str_list<'a>(vm: &mut Vm, parts: impl IntoIterator<Item = &'a str>) -> RtResult<Value> {
172    let parts = parts.into_iter();
173    let mut items = try_vec(parts.size_hint().0)?;
174    for p in parts {
175        let v = vm.m.try_alloc_str_nogc(p)?;
176        try_push(&mut items, v)?;
177    }
178    new_list(vm, items)
179}
180
181/// A snapshot of a list's items (with ownership checks), or a `MemoryError` panic.
182pub(crate) fn list_items(vm: &Vm, l: Value) -> RtResult<Vec<Value>> {
183    ownership::read_list(&vm.m, l)?.try_to_vec()
184}
185
186pub(crate) fn to_s(vm: &mut Vm, v: Value) -> RtResult<String> {
187    ops::to_s(vm, v)
188}
189
190pub(crate) fn ok(vm: &mut Vm, v: Value) -> Value {
191    catallaxy_vm::slow::make_result(vm, true, v)
192}
193
194pub(crate) fn err(vm: &mut Vm, msg: &str) -> Value {
195    let s = new_str(vm, msg);
196    catallaxy_vm::slow::make_result(vm, false, s)
197}
198
199/// `Err("path: error")` for a failed file operation, or a `MemoryError` panic if the data did
200/// not fit in memory (the standard library reports a refused buffer as an I/O error).
201pub(crate) fn io_err(vm: &mut Vm, path: &str, e: std::io::Error) -> RtResult<Value> {
202    if e.kind() == std::io::ErrorKind::OutOfMemory {
203        return catallaxy_runtime::refused(format!("cannot read {path} into memory"));
204    }
205    Ok(err(vm, &format!("{path}: {e}")))
206}
207
208pub(crate) fn exit_trap(code: i32) -> Trap {
209    Trap::Exit(code)
210}
211
212/// Runs a blocking operation with the thread in native state.
213pub(crate) fn blocking<R>(vm: &Vm, f: impl FnOnce() -> R) -> R {
214    flush();
215    catallaxy_gc::blocking(&vm.m.rec, f)
216}