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}