Skip to main content

cljrs_runtime/
runtime.rs

1//! The one construction path for a clojurust runtime.
2//!
3//! Before this stage every layer had its own "standard environment"
4//! constructor — `cljrs_runtime::interp::standard_env{,_minimal,_with_paths}`,
5//! `cljrs_runtime::tiered::standard_env{,_minimal,_minimal_no_ir,_with_paths}`, and
6//! `cljrs_stdlib::standard_env{,_no_ir,_with_paths,_with_paths_and_config}`.
7//! They differed in which `fn` pointers they installed, whether they enabled
8//! IR lowering, and which of GC config / root tracer / source paths they
9//! remembered to set, and callers had to know which one matched their needs.
10//!
11//! There is now one: [`Runtime::builder`].  Execution mode, source paths, GC
12//! configuration, embedded namespace sources, and tier enablement are all
13//! builder inputs.  Extensions install themselves into a finished runtime —
14//! `cljrs_stdlib::install(&runtime)` and friends.
15//!
16//! ```no_run
17//! use cljrs_runtime::{ExecutionMode, Runtime};
18//!
19//! let runtime = Runtime::builder()
20//!     .execution_mode(ExecutionMode::Tiered)
21//!     .source_paths(vec!["src".into()])
22//!     .build()
23//!     .expect("bootstrap");
24//!
25//! // Extensions install into the finished runtime; this package cannot name
26//! // them (they depend on it), so the call is shown rather than compiled:
27//! //     cljrs_stdlib::install(&runtime);
28//! let mut env = runtime.env("user");
29//! ```
30
31use std::path::PathBuf;
32use std::sync::Arc;
33
34use cljrs_gc::GcConfig;
35
36use crate::builtins::builtins;
37use crate::env::env::{Env, GlobalEnv};
38use crate::interp::{eval, special};
39use crate::mode::{ExecutionMode, TierState};
40
41/// Why a runtime could not be built.
42#[derive(Debug, thiserror::Error)]
43pub enum BuildError {
44    /// An embedded source that the builder evaluates could not be parsed.
45    /// This means the binary's own bootstrap text is broken.
46    #[error("failed to parse embedded source {origin}: {message}")]
47    EmbeddedSource { origin: String, message: String },
48}
49
50/// A constructed runtime: an environment plus the execution mode that decides
51/// how its code runs.
52///
53/// Cheap to clone — every runtime instance's state lives in the shared
54/// [`GlobalEnv`], so clones name the same runtime rather than a new one.
55#[derive(Clone, Debug)]
56pub struct Runtime {
57    globals: Arc<GlobalEnv>,
58}
59
60impl Runtime {
61    /// Start configuring a runtime.
62    pub fn builder() -> RuntimeBuilder {
63        RuntimeBuilder::new()
64    }
65
66    /// Adopt an already-constructed environment.
67    ///
68    /// For code that is handed an `Arc<GlobalEnv>` (an AOT harness, an
69    /// embedding host, a native package loader) and needs a [`Runtime`] to
70    /// pass to an extension's `install`.
71    pub fn from_globals(globals: Arc<GlobalEnv>) -> Self {
72        Self { globals }
73    }
74
75    /// The environment this runtime evaluates in.
76    pub fn globals(&self) -> &Arc<GlobalEnv> {
77        &self.globals
78    }
79
80    /// Take ownership of the environment handle.
81    pub fn into_globals(self) -> Arc<GlobalEnv> {
82        self.globals
83    }
84
85    /// A fresh evaluation context in namespace `ns`.
86    pub fn env(&self, ns: &str) -> Env {
87        Env::new(self.globals.clone(), ns)
88    }
89
90    /// How this runtime executes function calls.
91    pub fn execution_mode(&self) -> ExecutionMode {
92        self.globals.execution_mode()
93    }
94
95    /// Which tiers are live right now.
96    pub fn tier_state(&self) -> TierState {
97        self.globals.tier_state()
98    }
99}
100
101/// Configuration for [`Runtime::builder`].
102pub struct RuntimeBuilder {
103    execution_mode: ExecutionMode,
104    source_paths: Vec<PathBuf>,
105    gc_config: Option<Arc<GcConfig>>,
106    gc_config_from_env: bool,
107    register_gc_roots: bool,
108    builtin_sources: Vec<(String, &'static str)>,
109    eager_clojure_test: bool,
110}
111
112impl Default for RuntimeBuilder {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118impl RuntimeBuilder {
119    pub fn new() -> Self {
120        Self {
121            execution_mode: ExecutionMode::default(),
122            source_paths: Vec::new(),
123            gc_config: None,
124            gc_config_from_env: true,
125            register_gc_roots: true,
126            builtin_sources: Vec::new(),
127            eager_clojure_test: false,
128        }
129    }
130
131    /// Select how the runtime executes function calls.  Defaults to
132    /// [`ExecutionMode::Tiered`].
133    pub fn execution_mode(mut self, mode: ExecutionMode) -> Self {
134        self.execution_mode = mode;
135        self
136    }
137
138    /// Directories searched when `require` resolves a namespace to a file.
139    pub fn source_paths(mut self, paths: Vec<PathBuf>) -> Self {
140        self.source_paths = paths;
141        self
142    }
143
144    /// Explicit GC limits.  Without this the heap is configured from the
145    /// environment (`CLJRS_GC_*`), unless [`Self::gc_config_from_env`] is off.
146    pub fn gc_config(mut self, config: Arc<GcConfig>) -> Self {
147        self.gc_config = Some(config);
148        self
149    }
150
151    /// Whether to apply `CLJRS_GC_*` environment settings to the heap.
152    /// On by default; an explicit [`Self::gc_config`] is applied after it.
153    pub fn gc_config_from_env(mut self, enabled: bool) -> Self {
154        self.gc_config_from_env = enabled;
155        self
156    }
157
158    /// Whether to register this runtime's namespace table as a GC root set.
159    /// On by default.  The tracer holds a weak handle, so the runtime is
160    /// still collected when the last [`Runtime`] handle drops.
161    pub fn register_gc_roots(mut self, enabled: bool) -> Self {
162        self.register_gc_roots = enabled;
163        self
164    }
165
166    /// Embed a namespace's source in the runtime, so `require` resolves it
167    /// without a file on the source path.
168    pub fn builtin_source(mut self, ns: impl Into<String>, src: &'static str) -> Self {
169        self.builtin_sources.push((ns.into(), src));
170        self
171    }
172
173    /// Evaluate `clojure.test` during construction instead of leaving it to
174    /// the first `require`.
175    ///
176    /// Only useful without an extension that embeds `clojure.test` lazily
177    /// (`cljrs-stdlib` does); tests inside this package rely on it.
178    pub fn eager_clojure_test(mut self, enabled: bool) -> Self {
179        self.eager_clojure_test = enabled;
180        self
181    }
182
183    /// Bootstrap the runtime.
184    ///
185    /// Registers native `clojure.core`, evaluates the bootstrap Clojure
186    /// source, applies GC and source-path configuration, and finally raises
187    /// the tier state to what the execution mode targets — the bootstrap
188    /// itself always tree-walks, because nothing can be lowered before
189    /// `clojure.core` exists.
190    pub fn build(self) -> Result<Runtime, BuildError> {
191        let globals = GlobalEnv::new(self.execution_mode);
192
193        // Native clojure.core, then a `user` namespace referring it.
194        builtins::register_all(&globals, "clojure.core");
195        globals.get_or_create_ns("user");
196        globals.refer_all("user", "clojure.core");
197
198        // Bootstrap Clojure source (higher-order fns defined in Clojure).
199        eval_embedded(&globals, builtins::BOOTSTRAP_SOURCE, "<bootstrap>")?;
200
201        // Re-refer clojure.core now that the bootstrap has defined its HOFs.
202        globals.refer_all("user", "clojure.core");
203        globals.mark_loaded("clojure.core");
204
205        for (ns, src) in &self.builtin_sources {
206            globals.register_builtin_source(ns, src);
207        }
208
209        if self.eager_clojure_test {
210            eval_embedded(&globals, builtins::CLOJURE_TEST_SOURCE, "<clojure.test>")?;
211            globals.mark_loaded("clojure.test");
212        }
213
214        if !self.source_paths.is_empty() {
215            globals.set_source_paths(self.source_paths);
216        }
217
218        if self.gc_config_from_env {
219            cljrs_gc::HEAP.set_config_from_env();
220        }
221        if let Some(config) = self.gc_config {
222            globals.set_gc_config(config.clone());
223            cljrs_gc::HEAP.set_config(config);
224        }
225        if self.register_gc_roots {
226            register_namespace_roots(&globals);
227        }
228
229        // `*ns*` is `user` — loading above may have moved it.
230        special::sync_star_ns(&mut Env::new(globals.clone(), "user"));
231
232        // Bootstrap is over: raise the tiers this mode targets.  `CLJRS_NO_IR`
233        // pins the runtime at tree-walk regardless of mode.
234        if std::env::var("CLJRS_NO_IR").is_err() {
235            let target = self.execution_mode.target_tier();
236            if target.ir_enabled() {
237                // Functions defined before this point (the clojure.core
238                // bootstrap) stay excluded from background lowering.
239                globals
240                    .jit()
241                    .set_bootstrap_watermark(crate::interp::arity::next_arity_id());
242            }
243            globals.set_tier_state(target);
244        }
245
246        Ok(Runtime { globals })
247    }
248}
249
250/// Parse and evaluate an embedded source text in `clojure.core`.
251///
252/// A parse failure is fatal (the binary's own embedded text is broken); an
253/// individual form failing to evaluate is reported and skipped, which is the
254/// long-standing behavior of the bootstrap.
255fn eval_embedded(globals: &Arc<GlobalEnv>, src: &str, origin: &str) -> Result<(), BuildError> {
256    let mut parser = cljrs_reader::Parser::new(src.to_string(), origin.to_string());
257    let forms = parser.parse_all().map_err(|e| BuildError::EmbeddedSource {
258        origin: origin.to_string(),
259        message: format!("{e:?}"),
260    })?;
261    let mut env = Env::new(globals.clone(), "clojure.core");
262    for form in forms {
263        let _alloc_frame = cljrs_gc::push_alloc_frame();
264        if let Err(e) = eval::eval(&form, &mut env) {
265            eprintln!("[{origin} warning] {}: {:?}", form.span.start, e);
266        }
267    }
268    Ok(())
269}
270
271/// Register the runtime's namespace table as a GC root set.
272///
273/// The tracer holds a `Weak` handle: a runtime that is dropped stops being a
274/// root instead of keeping itself alive forever through the heap's tracer
275/// list, which matters now that a process can build several runtimes.
276fn register_namespace_roots(globals: &Arc<GlobalEnv>) {
277    let weak = Arc::downgrade(globals);
278    cljrs_gc::HEAP.register_root_tracer(move |visitor| {
279        use cljrs_gc::GcVisitor as _;
280        let Some(globals) = weak.upgrade() else {
281            return;
282        };
283        let namespaces = globals.namespaces.read().unwrap();
284        for ns_ptr in namespaces.values() {
285            visitor.visit(ns_ptr);
286        }
287    });
288}