Skip to main content

monkey_asm/
runtime.rs

1//! Native `extern "C"` runtime the generated `.s` links against (design §8).
2//!
3//! Every entry point is a thin shell: decode the raw FFI arguments, run the
4//! shared semantics from [`crate::runtime_core`] on a [`PointerStore`], and
5//! turn any [`RuntimeFailure`] into `rt_fatal` (observer error record when
6//! initialized, stderr message, `exit(1)`). Panics never cross the FFI
7//! boundary: shells run under `catch_unwind` and report `InternalError`.
8//!
9//! Heap objects are owned by one process-wide `PointerStore`. A mutex keeps
10//! its safe reference API exclusive across FFI entries and threads; generated
11//! function calls happen only after the guard has been released.
12
13use std::panic::{catch_unwind, AssertUnwindSafe};
14use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
15use std::sync::{Mutex, OnceLock};
16
17use crate::runtime_backend::{CodeHandle, PointerStore};
18use crate::runtime_core::{
19    self, CallDispatch, OutputSink, ReturnPolicy, RuntimeErrorKind, RuntimeFailure, RuntimeResult,
20    Value, NULL_VALUE,
21};
22
23/// Observer channel fd registered by `rt_observer_init`; -1 = not installed.
24static OBSERVER_FD: AtomicI64 = AtomicI64::new(-1);
25
26/// Globals area registered by `rt_globals_init` (future GC root scanning).
27static GLOBALS_BASE: AtomicU64 = AtomicU64::new(0);
28static GLOBALS_COUNT: AtomicU64 = AtomicU64::new(0);
29
30fn native_store() -> &'static Mutex<PointerStore> {
31    static STORE: OnceLock<Mutex<PointerStore>> = OnceLock::new();
32    STORE.get_or_init(|| Mutex::new(PointerStore::new()))
33}
34
35struct StdoutSink;
36
37impl OutputSink for StdoutSink {
38    fn write_line(&mut self, line: &str) {
39        use std::io::Write;
40        let stdout = std::io::stdout();
41        let mut handle = stdout.lock();
42        // stdout must stay the exact byte stream of puts/print (design §10.2);
43        // flush per line so exit(1)/exit paths cannot drop buffered bytes.
44        let _ = handle.write_all(line.as_bytes());
45        let _ = handle.write_all(b"\n");
46        let _ = handle.flush();
47    }
48}
49
50/// Writes the single framed observer record: u64 big-endian payload length,
51/// then the UTF-8 JSON payload (design §10.2).
52#[cfg(unix)]
53fn observer_write(payload: &str) {
54    use std::io::Write;
55    use std::os::unix::io::{FromRawFd, IntoRawFd};
56
57    let fd = OBSERVER_FD.load(Ordering::SeqCst);
58    if fd < 0 {
59        return;
60    }
61    let mut file = unsafe { std::fs::File::from_raw_fd(fd as i32) };
62    let length = (payload.len() as u64).to_be_bytes();
63    let _ = file.write_all(&length);
64    let _ = file.write_all(payload.as_bytes());
65    let _ = file.flush();
66    // The harness owns the fd; keep it open.
67    let _ = file.into_raw_fd();
68}
69
70#[cfg(not(unix))]
71fn observer_write(_payload: &str) {}
72
73/// Terminal error path shared by all shells (design §8): optional observer
74/// error record, human-readable stderr line, `exit(1)`.
75fn fatal(kind: RuntimeErrorKind, message: &str) -> ! {
76    observer_write(&format!("{{\"status\":\"error\",\"kind\":\"{}\"}}", kind.name()));
77    eprintln!("monkey: {}: {}", kind.name(), message);
78    std::process::exit(1);
79}
80
81/// Runs one FFI shell body: panics become `InternalError`, `RuntimeFailure`
82/// becomes `rt_fatal` semantics. Only `Ok` values return to generated code.
83fn ffi_shell<T>(body: impl FnOnce(&mut PointerStore) -> RuntimeResult<T>) -> T {
84    let outcome = catch_unwind(AssertUnwindSafe(|| {
85        let mut store = native_store()
86            .lock()
87            .unwrap_or_else(|poisoned| poisoned.into_inner());
88        body(&mut store)
89    }));
90    match outcome {
91        Ok(Ok(value)) => value,
92        Ok(Err(RuntimeFailure {
93            kind,
94            message,
95        })) => fatal(kind, &message),
96        Err(_) => fatal(RuntimeErrorKind::InternalError, "runtime panicked"),
97    }
98}
99
100/// `(ptr, len)` decoding per §8: zero length may be null and is an empty
101/// slice; non-zero length must be a valid, aligned region.
102unsafe fn value_slice<'a>(ptr: *const Value, len: u64) -> &'a [Value] {
103    if len == 0 {
104        &[]
105    } else {
106        std::slice::from_raw_parts(ptr, len as usize)
107    }
108}
109
110unsafe fn byte_slice<'a>(ptr: *const u8, len: u64) -> &'a [u8] {
111    if len == 0 {
112        &[]
113    } else {
114        std::slice::from_raw_parts(ptr, len as usize)
115    }
116}
117
118fn name_from_bytes(bytes: &[u8]) -> RuntimeResult<&str> {
119    std::str::from_utf8(bytes).map_err(|_| RuntimeFailure {
120        kind: RuntimeErrorKind::InternalError,
121        message: "identifier bytes are not valid UTF-8".to_string(),
122    })
123}
124
125/// Calls generated code for a `CallDispatch::Invoke` (design §8.1): pick the
126/// `extern "C"` signature by arity, closure in `x0`, arguments in `x1..x7`.
127unsafe fn invoke_code(code: CodeHandle, closure: Value, args: &[Value]) -> Value {
128    use std::mem::transmute;
129    let address = code as usize;
130    match args {
131        [] => transmute::<usize, extern "C" fn(Value) -> Value>(address)(closure),
132        [a1] => transmute::<usize, extern "C" fn(Value, Value) -> Value>(address)(closure, *a1),
133        [a1, a2] => transmute::<usize, extern "C" fn(Value, Value, Value) -> Value>(address)(
134            closure, *a1, *a2,
135        ),
136        [a1, a2, a3] => transmute::<usize, extern "C" fn(Value, Value, Value, Value) -> Value>(
137            address,
138        )(closure, *a1, *a2, *a3),
139        [a1, a2, a3, a4] => transmute::<
140            usize,
141            extern "C" fn(Value, Value, Value, Value, Value) -> Value,
142        >(address)(closure, *a1, *a2, *a3, *a4),
143        [a1, a2, a3, a4, a5] => transmute::<
144            usize,
145            extern "C" fn(Value, Value, Value, Value, Value, Value) -> Value,
146        >(address)(closure, *a1, *a2, *a3, *a4, *a5),
147        [a1, a2, a3, a4, a5, a6] => transmute::<
148            usize,
149            extern "C" fn(Value, Value, Value, Value, Value, Value, Value) -> Value,
150        >(address)(closure, *a1, *a2, *a3, *a4, *a5, *a6),
151        [a1, a2, a3, a4, a5, a6, a7] => {
152            transmute::<
153                usize,
154                extern "C" fn(Value, Value, Value, Value, Value, Value, Value, Value) -> Value,
155            >(address)(closure, *a1, *a2, *a3, *a4, *a5, *a6, *a7)
156        }
157        _ => fatal(
158            RuntimeErrorKind::ResourceLimit,
159            "call requires more arguments than the calling convention allows",
160        ),
161    }
162}
163
164/// Finishes a dispatch outside any store borrow: `Invoke` re-enters generated
165/// code, which will recursively call back into these shells.
166fn complete_dispatch(dispatch: CallDispatch) -> Value {
167    match dispatch {
168        CallDispatch::Return(value) => value,
169        CallDispatch::Invoke {
170            code,
171            closure,
172            args,
173            return_policy,
174        } => {
175            let returned = unsafe { invoke_code(code, closure, &args) };
176            match return_policy {
177                ReturnPolicy::Direct => returned,
178                ReturnPolicy::ConstructorInstance(instance) => instance,
179            }
180        }
181    }
182}
183
184#[no_mangle]
185/// # Safety
186/// `base` must be writable for `count` consecutive, properly aligned values
187/// and remain live for the generated program's execution.
188pub unsafe extern "C" fn rt_globals_init(base: *mut Value, count: u64) {
189    ffi_shell(|_store| {
190        for index in 0..count as usize {
191            unsafe {
192                *base.add(index) = NULL_VALUE;
193            }
194        }
195        GLOBALS_BASE.store(base as u64, Ordering::SeqCst);
196        GLOBALS_COUNT.store(count, Ordering::SeqCst);
197        Ok(())
198    })
199}
200
201#[no_mangle]
202/// # Safety
203/// For nonzero `len`, `ptr` must reference `len` readable bytes.
204pub unsafe extern "C" fn rt_string_from_bytes(ptr: *const u8, len: u64) -> Value {
205    ffi_shell(|store| {
206        let bytes = unsafe { byte_slice(ptr, len) };
207        runtime_core::string_from_utf8(store, bytes)
208    })
209}
210
211#[no_mangle]
212pub extern "C" fn rt_box_int(raw: i64) -> Value {
213    ffi_shell(|store| Ok(runtime_core::make_int(store, raw)))
214}
215
216#[no_mangle]
217/// # Safety
218/// For nonzero `len`, `argv` must reference `len` readable values.
219pub unsafe extern "C" fn rt_array(argv: *const Value, len: u64) -> Value {
220    ffi_shell(|store| {
221        let values = unsafe { value_slice(argv, len) };
222        Ok(runtime_core::array_from_values(store, values))
223    })
224}
225
226#[no_mangle]
227/// # Safety
228/// `argv` must reference `pairs * 2` readable values, and that multiplication
229/// must fit in `u64`.
230pub unsafe extern "C" fn rt_hash(argv: *const Value, pairs: u64) -> Value {
231    ffi_shell(|store| {
232        let values = unsafe { value_slice(argv, pairs * 2) };
233        runtime_core::hash_from_pairs(store, values)
234    })
235}
236
237#[no_mangle]
238/// # Safety
239/// `code` must be a generated function entry with the runtime calling
240/// convention. For nonzero `num_free`, `free` must reference that many values.
241pub unsafe extern "C" fn rt_closure(
242    code: *const u8,
243    num_parameters: u64,
244    free: *const Value,
245    num_free: u64,
246) -> Value {
247    ffi_shell(|store| {
248        let free_values = unsafe { value_slice(free, num_free) };
249        runtime_core::closure_new(store, code as CodeHandle, num_parameters, free_values)
250    })
251}
252
253#[no_mangle]
254pub extern "C" fn rt_get_free(closure: Value, index: u64) -> Value {
255    ffi_shell(|store| runtime_core::get_free(store, closure, index))
256}
257
258#[no_mangle]
259/// # Safety
260/// For nonzero `len`, `name` must reference `len` readable bytes.
261pub unsafe extern "C" fn rt_class(name: *const u8, len: u64) -> Value {
262    ffi_shell(|store| {
263        let bytes = unsafe { byte_slice(name, len) };
264        let class_name = name_from_bytes(bytes)?;
265        Ok(runtime_core::class_new(store, class_name))
266    })
267}
268
269#[no_mangle]
270/// # Safety
271/// For nonzero `len`, `name` must reference `len` readable bytes.
272pub unsafe extern "C" fn rt_class_add_method(
273    class: Value,
274    name: *const u8,
275    len: u64,
276    method: Value,
277    is_ctor: u64,
278) {
279    ffi_shell(|store| {
280        let bytes = unsafe { byte_slice(name, len) };
281        let method_name = name_from_bytes(bytes)?;
282        runtime_core::class_add_method(store, class, method_name, method, is_ctor != 0)
283    })
284}
285
286#[no_mangle]
287/// # Safety
288/// For nonzero `len`, `name` must reference `len` readable bytes.
289pub unsafe extern "C" fn rt_get_property(obj: Value, name: *const u8, len: u64) -> Value {
290    ffi_shell(|store| {
291        let bytes = unsafe { byte_slice(name, len) };
292        let property = name_from_bytes(bytes)?;
293        runtime_core::get_property(store, obj, property)
294    })
295}
296
297#[no_mangle]
298/// # Safety
299/// For nonzero `len`, `name` must reference `len` readable bytes.
300pub unsafe extern "C" fn rt_set_property(obj: Value, name: *const u8, len: u64, v: Value) {
301    ffi_shell(|store| {
302        let bytes = unsafe { byte_slice(name, len) };
303        let property = name_from_bytes(bytes)?;
304        runtime_core::set_property(store, obj, property, v)
305    })
306}
307
308#[no_mangle]
309pub extern "C" fn rt_index(obj: Value, idx: Value) -> Value {
310    ffi_shell(|store| runtime_core::index(store, obj, idx))
311}
312
313#[no_mangle]
314pub extern "C" fn rt_add(l: Value, r: Value) -> Value {
315    ffi_shell(|store| runtime_core::add(store, l, r))
316}
317
318#[no_mangle]
319pub extern "C" fn rt_sub(l: Value, r: Value) -> Value {
320    ffi_shell(|store| runtime_core::sub(store, l, r))
321}
322
323#[no_mangle]
324pub extern "C" fn rt_mul(l: Value, r: Value) -> Value {
325    ffi_shell(|store| runtime_core::mul(store, l, r))
326}
327
328#[no_mangle]
329pub extern "C" fn rt_div(l: Value, r: Value) -> Value {
330    ffi_shell(|store| runtime_core::div(store, l, r))
331}
332
333#[no_mangle]
334pub extern "C" fn rt_eq(l: Value, r: Value) -> Value {
335    ffi_shell(|store| runtime_core::eq_values(store, l, r).map(runtime_core::bool_value))
336}
337
338#[no_mangle]
339pub extern "C" fn rt_neq(l: Value, r: Value) -> Value {
340    ffi_shell(|store| {
341        runtime_core::eq_values(store, l, r).map(|equal| runtime_core::bool_value(!equal))
342    })
343}
344
345#[no_mangle]
346pub extern "C" fn rt_gt(l: Value, r: Value) -> Value {
347    ffi_shell(|store| runtime_core::gt(store, l, r))
348}
349
350#[no_mangle]
351pub extern "C" fn rt_minus(v: Value) -> Value {
352    ffi_shell(|store| runtime_core::minus(store, v))
353}
354
355#[no_mangle]
356pub extern "C" fn rt_bang(v: Value) -> Value {
357    ffi_shell(|_store| Ok(runtime_core::bang(v)))
358}
359
360#[no_mangle]
361pub extern "C" fn rt_truthy(v: Value) -> u64 {
362    ffi_shell(|_store| Ok(if runtime_core::truthy(v) { 1 } else { 0 }))
363}
364
365#[no_mangle]
366/// # Safety
367/// For nonzero `argc`, `argv` must reference `argc` readable values. Any
368/// closure reached through `callee` must contain a valid generated code entry.
369pub unsafe extern "C" fn rt_call(callee: Value, argc: u64, argv: *const Value) -> Value {
370    let dispatch = ffi_shell(|store| {
371        let args = unsafe { value_slice(argv, argc) };
372        runtime_core::dispatch_call(store, &mut StdoutSink, callee, args)
373    });
374    complete_dispatch(dispatch)
375}
376
377#[no_mangle]
378/// # Safety
379/// For nonzero `argc`, `argv` must reference `argc` readable values. Any
380/// constructor reached through `callee` must contain a valid code entry.
381pub unsafe extern "C" fn rt_construct(callee: Value, argc: u64, argv: *const Value) -> Value {
382    let dispatch = ffi_shell(|store| {
383        let args = unsafe { value_slice(argv, argc) };
384        runtime_core::dispatch_construct(store, callee, args)
385    });
386    complete_dispatch(dispatch)
387}
388
389#[no_mangle]
390/// # Safety
391/// `fd` must be a valid writable Unix file descriptor owned by the harness
392/// for as long as observer output can be emitted.
393pub unsafe extern "C" fn rt_observer_init(fd: u64) {
394    OBSERVER_FD.store(fd as i64, Ordering::SeqCst);
395}
396
397#[no_mangle]
398pub extern "C" fn rt_observe_result(v: Value) {
399    let payload = ffi_shell(|store| {
400        let value = runtime_core::canonical_value(store, v)?;
401        Ok(format!("{{\"status\":\"ok\",\"value\":{}}}", value))
402    });
403    observer_write(&payload);
404}
405
406#[no_mangle]
407/// # Safety
408/// For nonzero `len`, `msg` must reference `len` readable bytes.
409pub unsafe extern "C" fn rt_fatal(kind: u64, msg: *const u8, len: u64) -> ! {
410    let outcome = catch_unwind(|| {
411        let kind = RuntimeErrorKind::from_u64(kind).unwrap_or(RuntimeErrorKind::InternalError);
412        let bytes = unsafe { byte_slice(msg, len) };
413        (kind, String::from_utf8_lossy(bytes).into_owned())
414    });
415    match outcome {
416        Ok((kind, message)) => fatal(kind, &message),
417        Err(_) => fatal(RuntimeErrorKind::InternalError, "runtime panicked"),
418    }
419}