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 // Register this thread with the process memory governor (under its
192 // thread name, unless already registered) before the first allocation.
193 let _ = cljrs_gc::governor::current_control();
194 let globals = GlobalEnv::new(self.execution_mode);
195
196 // Native clojure.core, then a `user` namespace referring it.
197 builtins::register_all(&globals, "clojure.core");
198 globals.get_or_create_ns("user");
199 globals.refer_core("user");
200
201 // Bootstrap Clojure source (higher-order fns defined in Clojure).
202 eval_embedded(&globals, builtins::BOOTSTRAP_SOURCE, "<bootstrap>")?;
203
204 // Re-refer clojure.core now that the bootstrap has defined its HOFs.
205 globals.refer_core("user");
206 globals.mark_loaded("clojure.core");
207
208 // Experimental namespace: natives first, then the Clojure source that
209 // builds on them. Not referred into `user` — an explicit `require` is
210 // the only way in, so a feature probe for `clojure.core/future` and
211 // friends still (correctly) finds nothing.
212 builtins::register_experimental(&globals, builtins::EXPERIMENTAL_NS);
213 eval_embedded(
214 &globals,
215 builtins::EXPERIMENTAL_SOURCE,
216 "<cljrs.core.experimental>",
217 )?;
218 globals.mark_loaded(builtins::EXPERIMENTAL_NS);
219
220 for (ns, src) in &self.builtin_sources {
221 globals.register_builtin_source(ns, src);
222 }
223
224 if self.eager_clojure_test {
225 eval_embedded(&globals, builtins::CLOJURE_TEST_SOURCE, "<clojure.test>")?;
226 globals.mark_loaded("clojure.test");
227 }
228
229 if !self.source_paths.is_empty() {
230 globals.set_source_paths(self.source_paths);
231 }
232
233 if self.gc_config_from_env {
234 cljrs_gc::HEAP.set_config_from_env();
235 }
236 if let Some(config) = self.gc_config {
237 globals.set_gc_config(config.clone());
238 cljrs_gc::HEAP.set_config(config);
239 }
240 if self.register_gc_roots {
241 register_namespace_roots(&globals);
242 }
243
244 // `*ns*` is `user` — loading above may have moved it.
245 special::sync_star_ns(&mut Env::new(globals.clone(), "user"));
246
247 // Bootstrap is over: raise the tiers this mode targets. `CLJRS_NO_IR`
248 // pins the runtime at tree-walk regardless of mode.
249 if std::env::var("CLJRS_NO_IR").is_err() {
250 let target = self.execution_mode.target_tier();
251 if target.ir_enabled() {
252 // Functions defined before this point (the clojure.core
253 // bootstrap) stay excluded from background lowering.
254 globals
255 .jit()
256 .set_bootstrap_watermark(crate::interp::arity::next_arity_id());
257 }
258 globals.set_tier_state(target);
259 }
260
261 Ok(Runtime { globals })
262 }
263}
264
265/// Parse and evaluate an embedded source text in `clojure.core`.
266///
267/// A parse failure is fatal (the binary's own embedded text is broken); an
268/// individual form failing to evaluate is reported and skipped, which is the
269/// long-standing behavior of the bootstrap.
270fn eval_embedded(globals: &Arc<GlobalEnv>, src: &str, origin: &str) -> Result<(), BuildError> {
271 let mut parser = cljrs_reader::Parser::new(src.to_string(), origin.to_string());
272 let forms = parser.parse_all().map_err(|e| BuildError::EmbeddedSource {
273 origin: origin.to_string(),
274 message: format!("{e:?}"),
275 })?;
276 let mut env = Env::new(globals.clone(), "clojure.core");
277 for form in forms {
278 let _alloc_frame = cljrs_gc::push_alloc_frame();
279 if let Err(e) = eval::eval(&form, &mut env) {
280 eprintln!("[{origin} warning] {}: {:?}", form.span.start, e);
281 }
282 }
283 Ok(())
284}
285
286/// Register the runtime's namespace table as a GC root set.
287///
288/// The tracer holds a `Weak` handle: a runtime that is dropped stops being a
289/// root instead of keeping itself alive forever through the heap's tracer
290/// list, which matters now that a process can build several runtimes.
291fn register_namespace_roots(globals: &Arc<GlobalEnv>) {
292 let weak = Arc::downgrade(globals);
293 cljrs_gc::HEAP.register_root_tracer(move |visitor| {
294 use cljrs_gc::GcVisitor as _;
295 let Some(globals) = weak.upgrade() else {
296 return;
297 };
298 let namespaces = globals.namespaces.read().unwrap();
299 for ns_ptr in namespaces.values() {
300 visitor.visit(ns_ptr);
301 }
302 });
303}