Skip to main content

scheme_rs/
runtime.rs

1//! Scheme-rs core runtime.
2//!
3//! The [`Runtime`] struct initializes and stores the core runtime for
4//! scheme-rs. It contains a registry of libraries and the memory associated
5//! with the JIT compiled [`Procedures`](Procedure).
6
7use crate::{
8    ast::DefinitionBody,
9    cps::{Compile, Cps, codegen::RuntimeFunctionsBuilder},
10    env::{Environment, Global, TopLevelEnvironment},
11    exceptions::{Exception, raise},
12    gc::{Gc, GcInner, Trace, init_gc},
13    hashtables::EqualHashSet,
14    lists::{Pair, list_to_vec},
15    num,
16    ports::{BufferMode, Port, Transcoder},
17    proc::{
18        Application, ContinuationPtr, DynamicState, FuncPtr, ProcDebugInfo, Procedure, UserPtr,
19    },
20    registry::Registry,
21    symbols::Symbol,
22    syntax::{Identifier, Span, Syntax},
23    value::{Cell, UnpackedValue, Value},
24};
25use parking_lot::RwLock;
26use scheme_rs_macros::{maybe_async, maybe_await, runtime_fn};
27use std::{
28    collections::{BTreeSet, HashSet},
29    mem::ManuallyDrop,
30    path::Path,
31    sync::Arc,
32};
33
34/// Scheme-rs core runtime
35///
36/// Practically, the runtime is the core entry point for running Scheme programs
37/// with scheme-rs. It initializes the garbage collector and JIT compiler tasks
38/// and creates a new library registry.
39///
40/// There is not much you can do with a Runtime beyond creating it and using it
41/// to [run programs](Runtime::run_program), however a lot of functions require
42/// it as an arguments, such as [TopLevelEnvironment::new_repl].
43///
44/// You can also use the runtime to [define libraries](Runtime::def_lib) from
45/// Rust code.
46///
47/// Runtime is automatically reference counted, so if you have all of the
48/// procedures you need you can drop it without any issue.
49///
50/// # Safety:
51///
52/// The runtime contains the only live references to the Cranelift Context and
53/// therefore modules and allocated functions in the form a Sender of
54/// compilation tasks.
55///
56/// When that sender's ref count is zero, it will cause the receiver to fail and
57/// the compilation task will exit, allowing for a graceful shutdown.
58///
59/// However, this is dropping a lifetime. If we clone a procedure and drop the
60/// runtime from whence it was cleaved, we're left with a dangling pointer.
61///
62/// In order to remedy this it is vitally important the closure has a back
63/// pointer to the runtime.
64#[derive(Trace, Clone)]
65pub struct Runtime(pub(crate) Gc<RwLock<RuntimeInner>>);
66
67impl Default for Runtime {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73impl Runtime {
74    /// Creates a new runtime. Also initializes the garbage collector and
75    /// creates a default registry with the bridge functions populated.
76    pub fn new() -> Self {
77        let this = Self(Gc::new(RwLock::new(RuntimeInner::new())));
78        let new_registry = Registry::new(&this);
79        this.0.write().registry = new_registry;
80        this
81    }
82
83    /// Run a program at the given location and return the values.
84    #[maybe_async]
85    pub fn run_program(&self, path: &Path) -> Result<Vec<Value>, Exception> {
86        #[cfg(not(feature = "async"))]
87        use std::fs::File;
88
89        #[cfg(feature = "tokio")]
90        use tokio::fs::File;
91
92        let progm = TopLevelEnvironment::new_program(self, path);
93        let env = Environment::Top(progm.clone());
94        let mut form = {
95            let port = Port::new(
96                path.display(),
97                maybe_await!(File::open(path)).unwrap(),
98                BufferMode::Block,
99                Some(Transcoder::native()),
100            );
101            let file_name = path.file_name().unwrap().to_str().unwrap_or("<unknown>");
102            let span = Span::new(file_name);
103            maybe_await!(port.all_sexprs(span)).map_err(Exception::from)?
104        };
105
106        form.add_scope(progm.scope());
107        let body = maybe_await!(DefinitionBody::parse_lib_body(self, &form, &env))?;
108        let compiled = body.compile_top_level();
109        let closure = maybe_await!(self.compile_expr(compiled));
110
111        maybe_await!(Application::new(closure, Vec::new()).eval(&mut DynamicState::default()))
112    }
113
114    /// Define a library from Rust code. Useful if file system access is disabled.
115    #[cfg(not(feature = "async"))]
116    #[track_caller]
117    pub fn def_lib(&self, lib: &str) -> Result<(), Exception> {
118        use std::panic::Location;
119
120        self.get_registry()
121            .def_lib(self, lib, Location::caller().file())
122    }
123
124    /// Define a library from Rust code. Useful if file system access is disabled.
125    #[cfg(feature = "async")]
126    pub async fn def_lib(&self, lib: &str) -> Result<(), Exception> {
127        use std::panic::Location;
128
129        self.get_registry()
130            .def_lib(self, lib, Location::caller().file())
131            .await
132    }
133
134    pub(crate) fn get_registry(&self) -> Registry {
135        self.0.read().registry.clone()
136    }
137
138    #[maybe_async]
139    pub(crate) fn compile_expr(&self, expr: Cps) -> Procedure {
140        let (completion_tx, completion_rx) = completion();
141        let task = CompilationTask {
142            completion_tx,
143            compilation_unit: expr,
144            runtime: self.clone(),
145        };
146        let sender = { self.0.read().compilation_buffer_tx.clone() };
147        let _ = maybe_await!(sender.send(task));
148        // Wait for the compilation task to complete:
149        maybe_await!(recv_procedure(completion_rx))
150    }
151
152    pub(crate) unsafe fn from_raw_inc_rc(rt: *mut GcInner<RwLock<RuntimeInner>>) -> Self {
153        unsafe { Self(Gc::from_raw_inc_rc(rt)) }
154    }
155}
156
157#[cfg(not(feature = "async"))]
158type CompilationBufferTx = std::sync::mpsc::SyncSender<CompilationTask>;
159#[cfg(not(feature = "async"))]
160type CompilationBufferRx = std::sync::mpsc::Receiver<CompilationTask>;
161
162#[cfg(feature = "async")]
163type CompilationBufferTx = tokio::sync::mpsc::Sender<CompilationTask>;
164#[cfg(feature = "async")]
165type CompilationBufferRx = tokio::sync::mpsc::Receiver<CompilationTask>;
166
167#[derive(Trace)]
168pub(crate) struct RuntimeInner {
169    /// Package registry
170    pub(crate) registry: Registry,
171    /// Channel to compilation task
172    compilation_buffer_tx: CompilationBufferTx,
173    pub(crate) constants_pool: EqualHashSet,
174    pub(crate) globals_pool: HashSet<Global>,
175    pub(crate) debug_info: DebugInfo,
176}
177
178impl Default for RuntimeInner {
179    fn default() -> Self {
180        Self::new()
181    }
182}
183
184const MAX_COMPILATION_TASKS: usize = 5; // Shrug
185
186#[cfg(not(feature = "async"))]
187fn compilation_buffer() -> (CompilationBufferTx, CompilationBufferRx) {
188    std::sync::mpsc::sync_channel(MAX_COMPILATION_TASKS)
189}
190
191#[cfg(feature = "async")]
192fn compilation_buffer() -> (CompilationBufferTx, CompilationBufferRx) {
193    tokio::sync::mpsc::channel(MAX_COMPILATION_TASKS)
194}
195
196impl RuntimeInner {
197    fn new() -> Self {
198        // Ensure the GC is initialized:
199        init_gc();
200        let (compilation_buffer_tx, compilation_buffer_rx) = compilation_buffer();
201        // According the inkwell (and therefore LLVM docs), one LlvmContext may
202        // be present per thread. Thus, we spawn a new thread and a new
203        // compilation task for every Runtime:
204        std::thread::spawn(move || compilation_task(compilation_buffer_rx));
205        RuntimeInner {
206            registry: Registry::empty(),
207            compilation_buffer_tx,
208            constants_pool: EqualHashSet::new(),
209            globals_pool: HashSet::new(),
210            debug_info: DebugInfo::default(),
211        }
212    }
213}
214
215#[derive(Trace, Clone, Debug, Default)]
216pub(crate) struct DebugInfo {
217    /// Stored user function debug information:
218    stored_func_info: Vec<Arc<ProcDebugInfo>>,
219}
220
221impl DebugInfo {
222    pub fn store_func_info(&mut self, debug_info: Arc<ProcDebugInfo>) {
223        self.stored_func_info.push(debug_info);
224    }
225}
226
227#[cfg(not(feature = "async"))]
228type CompletionTx = std::sync::mpsc::SyncSender<Procedure>;
229#[cfg(not(feature = "async"))]
230type CompletionRx = std::sync::mpsc::Receiver<Procedure>;
231
232#[cfg(feature = "async")]
233type CompletionTx = tokio::sync::oneshot::Sender<Procedure>;
234#[cfg(feature = "async")]
235type CompletionRx = tokio::sync::oneshot::Receiver<Procedure>;
236
237#[cfg(not(feature = "async"))]
238fn completion() -> (CompletionTx, CompletionRx) {
239    std::sync::mpsc::sync_channel(1)
240}
241
242#[cfg(feature = "async")]
243fn completion() -> (CompletionTx, CompletionRx) {
244    tokio::sync::oneshot::channel()
245}
246
247#[cfg(not(feature = "async"))]
248fn recv_procedure(rx: CompletionRx) -> Procedure {
249    rx.recv().unwrap()
250}
251
252#[cfg(feature = "async")]
253async fn recv_procedure(rx: CompletionRx) -> Procedure {
254    rx.await.unwrap()
255}
256
257struct CompilationTask {
258    compilation_unit: Cps,
259    completion_tx: CompletionTx,
260    /// Since Contexts are per-thread, we will only ever see the same Runtime.
261    /// However, we can't cache the Runtime, as that would cause a ref cycle
262    /// that would prevent the last compilation buffer sender to drop.
263    /// Therefore, its lifetime is that of the compilation task
264    runtime: Runtime,
265}
266
267#[cfg(not(feature = "async"))]
268fn recv_compilation_task(rx: &mut CompilationBufferRx) -> Option<CompilationTask> {
269    rx.recv().ok()
270}
271
272#[cfg(feature = "async")]
273fn recv_compilation_task(rx: &mut CompilationBufferRx) -> Option<CompilationTask> {
274    rx.blocking_recv()
275}
276
277fn compilation_task(mut compilation_queue_rx: CompilationBufferRx) {
278    use cranelift::prelude::*;
279    use cranelift_jit::{JITBuilder, JITModule};
280
281    let mut flag_builder = settings::builder();
282    flag_builder.set("use_colocated_libcalls", "false").unwrap();
283    // FIXME set back to true once the x64 backend supports it.
284    flag_builder.set("is_pic", "false").unwrap();
285    let isa_builder = cranelift_native::builder().unwrap_or_else(|msg| {
286        panic!("host machine is not supported: {msg}");
287    });
288    let isa = isa_builder
289        .finish(settings::Flags::new(flag_builder))
290        .unwrap();
291
292    let mut jit_builder = JITBuilder::with_isa(isa, cranelift_module::default_libcall_names());
293
294    for runtime_fn in inventory::iter::<RuntimeFn> {
295        (runtime_fn.install_symbol)(&mut jit_builder);
296    }
297
298    let mut module = JITModule::new(jit_builder);
299    let mut runtime_funcs_builder = RuntimeFunctionsBuilder::default();
300
301    for runtime_fn in inventory::iter::<RuntimeFn> {
302        (runtime_fn.install_decl)(&mut runtime_funcs_builder, &mut module);
303    }
304
305    let runtime_funcs = runtime_funcs_builder.build().unwrap();
306
307    // By storing all of the debug information in the same lifetime as the
308    // Context, we can directly put pointers referencing the debug information
309    // in our JIT compiled functions:
310    let mut debug_info = DebugInfo::default();
311
312    while let Some(task) = recv_compilation_task(&mut compilation_queue_rx) {
313        let CompilationTask {
314            completion_tx,
315            compilation_unit,
316            runtime,
317        } = task;
318
319        let proc =
320            compilation_unit.into_procedure(runtime, &runtime_funcs, &mut module, &mut debug_info);
321
322        let _ = completion_tx.send(proc);
323    }
324
325    // Free the JITed memory
326    unsafe {
327        module.free_memory();
328    }
329}
330
331pub(crate) struct RuntimeFn {
332    install_decl:
333        for<'a> fn(&'a mut RuntimeFunctionsBuilder, module: &'a mut cranelift_jit::JITModule),
334    install_symbol: for<'a> fn(&'a mut cranelift_jit::JITBuilder),
335}
336
337impl RuntimeFn {
338    pub(crate) const fn new(
339        install_decl: for<'a> fn(
340            &'a mut RuntimeFunctionsBuilder,
341            module: &'a mut cranelift_jit::JITModule,
342        ),
343        install_symbol: for<'a> fn(&'a mut cranelift_jit::JITBuilder),
344    ) -> Self {
345        Self {
346            install_decl,
347            install_symbol,
348        }
349    }
350}
351
352inventory::collect!(RuntimeFn);
353
354unsafe fn arc_from_ptr<T>(ptr: *const T) -> Option<Arc<T>> {
355    unsafe {
356        if ptr.is_null() {
357            return None;
358        }
359        Arc::increment_strong_count(ptr);
360        Some(Arc::from_raw(ptr))
361    }
362}
363
364/// Allocate a new Gc with a value of undefined
365#[runtime_fn]
366unsafe extern "C" fn alloc_cell() -> *const () {
367    Value::into_raw(Value::from(Cell(Gc::new(RwLock::new(Value::undefined())))))
368}
369
370/// Read the value of a Cell
371#[runtime_fn]
372unsafe extern "C" fn read_cell(cell: *const ()) -> *const () {
373    unsafe {
374        let cell = Value::from_raw(cell);
375        let cell: Cell = cell.try_into().unwrap();
376        // We do not need to increment the reference count of the cell, it is going to
377        // be decremented at the end of this function.
378        let cell = ManuallyDrop::new(cell);
379        let cell_read = cell.0.read();
380        Value::as_raw(&cell_read)
381    }
382}
383
384/// Decrement the reference count of a value
385#[runtime_fn]
386unsafe extern "C" fn dropv(val: *const *const (), num_drops: u32) {
387    unsafe {
388        for i in 0..num_drops {
389            drop(Value::from_raw(val.add(i as usize).read()));
390        }
391    }
392}
393
394/// Create a boxed application
395#[runtime_fn]
396unsafe extern "C" fn apply(
397    runtime: *mut GcInner<RwLock<RuntimeInner>>,
398    op: *const (),
399    args: *const *const (),
400    num_args: u32,
401    dyn_state: *mut DynamicState,
402) -> *mut Application {
403    unsafe {
404        let args: Vec<_> = (0..num_args)
405            .map(|i| Value::from_raw_inc_rc(args.add(i as usize).read()))
406            .collect();
407
408        let op = match Value::from_raw_inc_rc(op).unpack() {
409            UnpackedValue::Procedure(op) => op,
410            x => {
411                let raised = raise(
412                    Runtime::from_raw_inc_rc(runtime),
413                    Exception::invalid_operator(x.type_name()).into(),
414                    dyn_state.as_mut().unwrap_unchecked(),
415                );
416                return Box::into_raw(Box::new(raised));
417            }
418        };
419
420        let app = Application::new(op, args);
421
422        Box::into_raw(Box::new(app))
423    }
424}
425
426/// Get a frame from a procedure and a span
427#[runtime_fn]
428unsafe extern "C" fn get_frame(op: *const (), span: *const ()) -> *const () {
429    unsafe {
430        let op = Value::from_raw_inc_rc(op);
431        let Some(op) = op.cast_to_scheme_type::<Procedure>() else {
432            return Value::into_raw(Value::null());
433        };
434        let span = Value::from_raw_inc_rc(span);
435        let span = span.cast_to_rust_type::<Span>().unwrap();
436        let frame = Syntax::Identifier {
437            ident: Identifier {
438                sym: op
439                    .get_debug_info()
440                    .map_or_else(|| Symbol::intern("<lambda>"), |dbg| dbg.name),
441                scopes: BTreeSet::new(),
442            },
443            span: span.as_ref().clone(),
444        };
445        Value::into_raw(Value::from(frame))
446    }
447}
448
449/// Set the value for continuation mark
450#[runtime_fn]
451unsafe extern "C" fn set_continuation_mark(
452    tag: *const (),
453    val: *const (),
454    dyn_state: *mut DynamicState,
455) {
456    unsafe {
457        let tag = Value::from_raw_inc_rc(tag);
458        let val = Value::from_raw_inc_rc(val);
459        dyn_state
460            .as_mut()
461            .unwrap()
462            .set_continuation_mark(tag.cast_to_scheme_type().unwrap(), val);
463    }
464}
465
466/// Create a boxed application that simply returns its arguments
467#[runtime_fn]
468pub(crate) unsafe extern "C" fn halt(args: *const ()) -> *mut Application {
469    unsafe {
470        // We do not need to increment the rc here, it will be incremented in list_to_vec
471        let args = ManuallyDrop::new(Value::from_raw(args));
472        let mut flattened = Vec::new();
473        list_to_vec(&args, &mut flattened);
474        let app = Application::halt_ok(flattened);
475        Box::into_raw(Box::new(app))
476    }
477}
478
479/// Evaluate a `Gc<Value>` as "truthy" or not, as in whether it triggers a
480/// conditional.
481#[runtime_fn]
482unsafe extern "C" fn truthy(val: *const ()) -> bool {
483    unsafe {
484        // No need to increment the reference count here:
485        ManuallyDrop::new(Value::from_raw(val)).is_true()
486    }
487}
488
489/// Replace the value pointed to at to with the value contained in from.
490#[runtime_fn]
491unsafe extern "C" fn store(from: *const (), to: *const ()) {
492    unsafe {
493        // We do not need to increment the ref count for to, it is dropped
494        // immediately.
495        let from = Value::from_raw_inc_rc(from);
496        let to: ManuallyDrop<Cell> = ManuallyDrop::new(Value::from_raw(to).try_into().unwrap());
497        *to.0.write() = from;
498    }
499}
500
501/// Return the cons of the two arguments
502#[runtime_fn]
503unsafe extern "C" fn cons(vals: *const *const (), num_vals: u32, error: *mut Value) -> *const () {
504    unsafe {
505        if num_vals != 2 {
506            error.write(Exception::wrong_num_of_args(2, num_vals as usize).into());
507            return Value::into_raw(Value::undefined());
508        }
509        let car = Value::from_raw_inc_rc(vals.read());
510        let cdr = Value::from_raw_inc_rc(vals.add(1).read());
511        Value::into_raw(Value::from(Pair::new(car, cdr, true)))
512    }
513}
514
515/// Return the proper list of the arguments
516#[runtime_fn]
517unsafe extern "C" fn list(vals: *const *const (), num_vals: u32, _error: *mut Value) -> *const () {
518    let mut list = Value::null();
519    unsafe {
520        for i in (0..num_vals).rev() {
521            list = Value::from(Pair::new(
522                Value::from_raw_inc_rc(vals.add(i as usize).read()),
523                list,
524                true,
525            ));
526        }
527    }
528    Value::into_raw(list)
529}
530
531/// Allocate a continuation
532#[runtime_fn]
533unsafe extern "C" fn make_continuation(
534    runtime: *mut GcInner<RwLock<RuntimeInner>>,
535    fn_ptr: ContinuationPtr,
536    env: *const *const (),
537    num_envs: u32,
538    num_required_args: u32,
539    variadic: bool,
540    dyn_state: *mut DynamicState,
541) -> *const () {
542    unsafe {
543        // Collect the environment:
544        let env: Vec<_> = (0..num_envs)
545            .map(|i| Value::from_raw_inc_rc(env.add(i as usize).read()))
546            .collect();
547
548        let proc = dyn_state.as_mut().unwrap().new_k(
549            Runtime::from_raw_inc_rc(runtime),
550            env,
551            fn_ptr,
552            num_required_args as usize,
553            variadic,
554        );
555
556        Value::into_raw(Value::from(proc))
557    }
558}
559
560/// Allocate a user function
561#[runtime_fn]
562unsafe extern "C" fn make_user(
563    runtime: *mut GcInner<RwLock<RuntimeInner>>,
564    fn_ptr: UserPtr,
565    env: *const *const (),
566    num_envs: u32,
567    num_required_args: u32,
568    variadic: bool,
569    debug_info: *const ProcDebugInfo,
570) -> *const () {
571    unsafe {
572        // Collect the environment:
573        let env: Vec<_> = (0..num_envs)
574            .map(|i| Value::from_raw_inc_rc(env.add(i as usize).read()))
575            .collect();
576
577        let proc = Procedure::with_debug_info(
578            Runtime::from_raw_inc_rc(runtime),
579            env,
580            FuncPtr::User(fn_ptr),
581            num_required_args as usize,
582            variadic,
583            arc_from_ptr(debug_info),
584        );
585
586        Value::into_raw(Value::from(proc))
587    }
588}
589
590/// Return an error in the case that a value is undefined
591#[runtime_fn]
592unsafe extern "C" fn error_unbound_variable(symbol: u32) -> *const () {
593    let sym = Symbol(symbol);
594    let condition = Exception::error(format!("{sym} is unbound"));
595    Value::into_raw(Value::from(condition))
596}
597
598#[runtime_fn]
599unsafe extern "C" fn add(vals: *const *const (), num_vals: u32, error: *mut Value) -> *const () {
600    unsafe {
601        let vals: Vec<_> = (0..num_vals)
602            // Can't easily wrap these in a ManuallyDrop, so we dec the rc.
603            .map(|i| Value::from_raw_inc_rc(vals.add(i as usize).read()))
604            .collect();
605        match num::add_prim(&vals) {
606            Ok(num) => Value::into_raw(Value::from(num)),
607            Err(condition) => {
608                error.write(condition.into());
609                Value::into_raw(Value::undefined())
610            }
611        }
612    }
613}
614
615#[runtime_fn]
616unsafe extern "C" fn sub(vals: *const *const (), num_vals: u32, error: *mut Value) -> *const () {
617    unsafe {
618        let vals: Vec<_> = (0..num_vals)
619            .map(|i| Value::from_raw_inc_rc(vals.add(i as usize).read()))
620            .collect();
621        match num::sub_prim(&vals[0], &vals[1..]) {
622            Ok(num) => Value::into_raw(Value::from(num)),
623            Err(condition) => {
624                error.write(condition.into());
625                Value::into_raw(Value::undefined())
626            }
627        }
628    }
629}
630
631#[runtime_fn]
632unsafe extern "C" fn mul(vals: *const *const (), num_vals: u32, error: *mut Value) -> *const () {
633    unsafe {
634        let vals: Vec<_> = (0..num_vals)
635            .map(|i| Value::from_raw_inc_rc(vals.add(i as usize).read()))
636            .collect();
637        match num::mul_prim(&vals) {
638            Ok(num) => Value::into_raw(Value::from(num)),
639            Err(condition) => {
640                error.write(condition.into());
641                Value::into_raw(Value::undefined())
642            }
643        }
644    }
645}
646
647#[runtime_fn]
648unsafe extern "C" fn div(vals: *const *const (), num_vals: u32, error: *mut Value) -> *const () {
649    unsafe {
650        let vals: Vec<_> = (0..num_vals)
651            .map(|i| Value::from_raw_inc_rc(vals.add(i as usize).read()))
652            .collect();
653        match num::div_prim(&vals[0], &vals[1..]) {
654            Ok(num) => Value::into_raw(Value::from(num)),
655            Err(condition) => {
656                error.write(condition.into());
657                Value::into_raw(Value::undefined())
658            }
659        }
660    }
661}
662
663macro_rules! define_comparison_fn {
664    ( $name:ident, $prim:ident ) => {
665        #[runtime_fn]
666        unsafe extern "C" fn $name(
667            vals: *const *const (),
668            num_vals: u32,
669            error: *mut Value,
670        ) -> *const () {
671            unsafe {
672                let vals: Vec<_> = (0..num_vals)
673                    .map(|i| Value::from_raw_inc_rc(vals.add(i as usize).read()))
674                    .collect();
675                match num::$prim(&vals) {
676                    Ok(res) => Value::into_raw(Value::from(res)),
677                    Err(condition) => {
678                        error.write(condition.into());
679                        Value::into_raw(Value::undefined())
680                    }
681                }
682            }
683        }
684    };
685}
686
687define_comparison_fn!(equal, equal_prim);
688define_comparison_fn!(greater, greater_prim);
689define_comparison_fn!(greater_equal, greater_equal_prim);
690define_comparison_fn!(lesser, lesser_prim);
691define_comparison_fn!(lesser_equal, lesser_equal_prim);