Skip to main content

cljrs_stdlib/
lib.rs

1//! Built-in standard library namespaces for clojurust.
2//!
3//! Registers `clojure.string`, `clojure.set`, and `clojure.test` into a
4//! [`GlobalEnv`] so they are available via `(require ...)` without needing
5//! source files on disk.
6//!
7//! ## Entry points
8//!
9//! - [`standard_env()`] — full environment for the `cljrs` binary
10//! - [`standard_env_with_paths()`] — same, plus user source paths
11//! - [`register()`] — add stdlib to an existing env (e.g. for testing)
12
13use std::sync::Arc;
14
15use cljrs_eval::GlobalEnv;
16#[cfg(not(target_arch = "wasm32"))]
17use cljrs_gc::GcConfig;
18
19mod core_async;
20// io and edn use std::fs which is not available on wasm32-unknown-unknown
21#[cfg(not(target_arch = "wasm32"))]
22mod edn;
23#[cfg(not(target_arch = "wasm32"))]
24pub mod io;
25mod set;
26mod string;
27// ── Embedded sources ──────────────────────────────────────────────────────────
28
29const CLOJURE_TEST_SRC: &str = include_str!("clojure/test.cljrs");
30const CLOJURE_STRING_SRC: &str = include_str!("clojure/string.cljrs");
31const CLOJURE_SET_SRC: &str = include_str!("clojure/set.cljrs");
32const CLOJURE_TEMPLATE_SRC: &str = include_str!("clojure/template.cljrs");
33#[cfg(not(target_arch = "wasm32"))]
34const CLOJURE_RUST_IO_SRC: &str = include_str!("clojure/rust/io.cljrs");
35#[cfg(not(target_arch = "wasm32"))]
36const CLOJURE_EDN_SRC: &str = include_str!("clojure/edn.cljrs");
37const CLOJURE_WALK_SRC: &str = include_str!("clojure/walk.cljrs");
38const CLOJURE_DATA_SRC: &str = include_str!("clojure/data.cljrs");
39const COLJURE_ZIP_SRC: &str = include_str!("clojure/zip.cljrs");
40
41// ── Macro: register a batch of native fns into a namespace ───────────────────
42
43/// Register a slice of `(name, arity, fn)` triples as `NativeFunction` values
44/// in `$globals` under namespace `$ns`.
45macro_rules! register_fns {
46    ($globals:expr, $ns:expr, [ $( ($name:expr, $arity:expr, $func:expr) ),* $(,)? ]) => {{
47        use cljrs_gc::GcPtr;
48        use cljrs_value::{NativeFn, Value};
49        let ns: &str = $ns;
50        $(
51            {
52                let nf = NativeFn::new($name, $arity, $func);
53                $globals.intern(ns, std::sync::Arc::from($name), Value::NativeFunction(GcPtr::new(nf)));
54            }
55        )*
56    }};
57}
58
59pub(crate) use register_fns;
60
61// ── Public API ────────────────────────────────────────────────────────────────
62
63/// Register all built-in stdlib namespaces into `globals`.
64///
65/// This is idempotent: calling it again does not re-evaluate sources
66/// (already-loaded guard in `load_ns` prevents that), but it will
67/// overwrite native fn registrations in the namespace tables.
68/// In practice, call it once right after `standard_env_minimal()`.
69pub fn register(globals: &Arc<GlobalEnv>) {
70    // clojure.string ─ pre-register native fns, then register source for
71    // the lazy (ns clojure.string) form to run on first require.
72    string::register(globals, "clojure.string");
73    globals.register_builtin_source("clojure.string", CLOJURE_STRING_SRC);
74
75    // clojure.set ─ same pattern.
76    set::register(globals, "clojure.set");
77    globals.register_builtin_source("clojure.set", CLOJURE_SET_SRC);
78
79    // clojure.template ─ pure Clojure, no native helpers.
80    globals.register_builtin_source("clojure.template", CLOJURE_TEMPLATE_SRC);
81
82    // clojure.test ─ pure Clojure, no native helpers.
83    globals.register_builtin_source("clojure.test", CLOJURE_TEST_SRC);
84
85    // clojure.rust.io and clojure.edn use std::fs, unavailable on wasm32.
86    #[cfg(not(target_arch = "wasm32"))]
87    {
88        io::register(globals, "clojure.rust.io");
89        globals.register_builtin_source("clojure.rust.io", CLOJURE_RUST_IO_SRC);
90
91        edn::register(globals, "clojure.edn");
92        globals.register_builtin_source("clojure.edn", CLOJURE_EDN_SRC);
93    }
94
95    // clojure.walk ─ pure Clojure, no native helpers.
96    globals.register_builtin_source("clojure.walk", CLOJURE_WALK_SRC);
97
98    // clojure.data ─ pure Clojure, no native helpers.
99    globals.register_builtin_source("clojure.data", CLOJURE_DATA_SRC);
100
101    // clojure.zip
102    globals.register_builtin_source("clojure.zip", COLJURE_ZIP_SRC);
103}
104
105/// Prebuilt IR bundle for clojure.core (generated at build time).
106/// When the `prebuild-ir` feature is enabled, this contains serialized IR
107/// for all lowerable functions; otherwise it's an empty slice.
108#[cfg(feature = "prebuild-ir")]
109static PREBUILT_IR: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/core_ir.bin"));
110
111/// Prebuilt IR bundle for the five cljrs.compiler.* namespaces.
112/// Loaded into the IR cache after ensure_compiler_loaded() populates the
113/// namespace vars so the compiler itself runs in IR mode on hot paths.
114#[cfg(feature = "prebuild-ir")]
115static PREBUILT_COMPILER_IR: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/compiler_ir.bin"));
116
117/// Load the prebuilt compiler IR bundle into the IR cache.
118///
119/// Called after `ensure_compiler_loaded` so that the compiler namespace vars
120/// (with their runtime `ir_arity_id`s) already exist in `globals`.
121/// `load_prebuilt_ir` matches bundle keys (`"ns/name:arity"`) to live arity
122/// objects and stores the pre-built IR in the cache, replacing tree-walking
123/// for the hot compiler code paths.
124#[cfg(feature = "prebuild-ir")]
125fn load_prebuilt_compiler_ir(globals: &Arc<GlobalEnv>) {
126    match cljrs_ir::deserialize_bundle(PREBUILT_COMPILER_IR) {
127        Ok(bundle) if !bundle.is_empty() => {
128            let n = cljrs_eval::load_prebuilt_ir(globals, &bundle);
129            cljrs_logging::feat_debug!(
130                "ir",
131                "loaded {n} prebuilt compiler IR arities from bundle ({} entries)",
132                bundle.len()
133            );
134        }
135        Ok(_) => {}
136        Err(e) => {
137            cljrs_logging::feat_debug!("ir", "compiler IR bundle deserialize failed: {e}");
138        }
139    }
140}
141
142#[cfg(not(feature = "prebuild-ir"))]
143fn load_prebuilt_compiler_ir(_globals: &Arc<GlobalEnv>) {}
144
145/// Create a `GlobalEnv` with all built-ins and stdlib registered, **without**
146/// the IR lowering hook or compiler loading.
147///
148/// Use this in the AOT test harness and any other execution context where
149/// IR generation is not needed.  It avoids populating the global `IR_CACHE`
150/// with entries for test-namespace functions (entries that would never be
151/// evicted and would accumulate to hundreds of MB over 233 namespaces).
152/// It also skips loading the cljrs.compiler.* namespaces, saving additional
153/// startup time and memory.
154///
155/// GC config and root tracer are still registered identically to `standard_env`.
156#[cfg(not(target_arch = "wasm32"))]
157pub fn standard_env_no_ir() -> Arc<GlobalEnv> {
158    let globals = cljrs_eval::standard_env_minimal_no_ir();
159    register(&globals);
160
161    cljrs_gc::HEAP.set_config_from_env();
162    let roots_gc = globals.clone();
163    cljrs_gc::HEAP.register_root_tracer(move |visitor| {
164        use cljrs_gc::GcVisitor as _;
165        let namespaces = roots_gc.namespaces.read().unwrap();
166        for (_name, ns_ptr) in namespaces.iter() {
167            visitor.visit(ns_ptr);
168        }
169    });
170
171    globals
172}
173
174/// Create a `GlobalEnv` with all built-ins and stdlib registered.
175///
176/// Prefer this over `cljrs_eval::standard_env()` in the `cljrs` binary so that
177/// stdlib namespaces are loaded lazily (only on first `require`) instead of
178/// eagerly at startup.
179#[cfg(not(target_arch = "wasm32"))]
180pub fn standard_env() -> Arc<GlobalEnv> {
181    let globals = cljrs_eval::standard_env_minimal();
182    register(&globals);
183
184    // Configure GC with default limits and register namespace bindings as roots.
185    // Without this, the GC never fires (no config → no soft-limit check).
186    // standard_env_with_paths_and_config() overrides the config but reuses this tracer.
187    cljrs_gc::HEAP.set_config_from_env();
188    let roots_gc = globals.clone();
189    cljrs_gc::HEAP.register_root_tracer(move |visitor| {
190        use cljrs_gc::GcVisitor as _;
191        let namespaces = roots_gc.namespaces.read().unwrap();
192        for (_name, ns_ptr) in namespaces.iter() {
193            visitor.visit(ns_ptr);
194        }
195    });
196
197    // Try loading prebuilt IR first (skips compiler loading entirely).
198    #[cfg(feature = "prebuild-ir")]
199    {
200        match cljrs_ir::deserialize_bundle(PREBUILT_IR) {
201            Ok(bundle) if !bundle.is_empty() => {
202                // Register compiler sources (needed for any functions not in the
203                // prebuilt bundle, and for user code that triggers IR lowering).
204                cljrs_eval::register_compiler_sources(&globals);
205
206                let loaded = cljrs_eval::load_prebuilt_ir(&globals, &bundle);
207                cljrs_logging::feat_debug!(
208                    "ir",
209                    "loaded {loaded} prebuilt IR arities from bundle ({} entries)",
210                    bundle.len()
211                );
212
213                // Still load the compiler on a background thread so new user
214                // functions can be eagerly lowered at definition time.
215                // Once loaded, populate the IR cache with prebuilt compiler IR
216                // so the compiler itself runs in IR mode on hot paths.
217                let g = globals.clone();
218                let _ = std::thread::Builder::new()
219                    .stack_size(16 * 1024 * 1024)
220                    .spawn(move || {
221                        let mut env = cljrs_eval::Env::new(g.clone(), "user");
222                        if cljrs_eval::ensure_compiler_loaded(&g, &mut env) {
223                            load_prebuilt_compiler_ir(&g);
224                        }
225                    });
226
227                return globals;
228            }
229            _ => {
230                // Empty or corrupt bundle — fall through to runtime lowering.
231            }
232        }
233    }
234
235    // Fallback: register and load compiler namespaces so IR lowering is
236    // available at runtime. Loading runs on a thread with a large stack
237    // because the Clojure compiler sources are deeply recursive.
238    cljrs_eval::register_compiler_sources(&globals);
239    {
240        let g = globals.clone();
241        let _ = std::thread::Builder::new()
242            .stack_size(16 * 1024 * 1024)
243            .spawn(move || {
244                let mut env = cljrs_eval::Env::new(g.clone(), "user");
245                if cljrs_eval::ensure_compiler_loaded(&g, &mut env) {
246                    load_prebuilt_compiler_ir(&g);
247                }
248            })
249            .and_then(|h| h.join().map_err(|_| std::io::Error::other("join failed")));
250    }
251
252    globals
253}
254
255/// Like [`standard_env()`] but also sets user source paths for `require`.
256#[cfg(not(target_arch = "wasm32"))]
257pub fn standard_env_with_paths(source_paths: Vec<std::path::PathBuf>) -> Arc<GlobalEnv> {
258    let globals = standard_env();
259    globals.set_source_paths(source_paths);
260    globals
261}
262
263/// Like [`standard_env_with_paths()`] but also sets GC configuration.
264#[cfg(not(target_arch = "wasm32"))]
265pub fn standard_env_with_paths_and_config(
266    source_paths: Vec<std::path::PathBuf>,
267    gc_config: Arc<GcConfig>,
268) -> Arc<GlobalEnv> {
269    let globals = standard_env();
270    globals.set_source_paths(source_paths);
271    globals.set_gc_config(gc_config.clone());
272    // Override the default GC config set by standard_env() with the custom limits.
273    // The root tracer is already registered by standard_env().
274    cljrs_gc::HEAP.set_config(gc_config);
275    globals
276}
277
278// ── Tests ─────────────────────────────────────────────────────────────────────
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use cljrs_eval::{Env, EvalResult, eval};
284    use cljrs_reader::Parser;
285    use cljrs_value::Value;
286
287    fn make_env() -> (Arc<GlobalEnv>, Env) {
288        let globals = standard_env();
289        let env = Env::new(globals.clone(), "user");
290        (globals, env)
291    }
292
293    #[allow(clippy::result_large_err)]
294    fn run(src: &str, env: &mut Env) -> EvalResult {
295        let mut parser = Parser::new(src.to_string(), "<test>".to_string());
296        let forms = parser.parse_all().expect("parse error");
297        let mut result = Value::Nil;
298        for form in forms {
299            result = eval(&form, env)?;
300        }
301        Ok(result)
302    }
303
304    // ── clojure.string ────────────────────────────────────────────────────────
305
306    #[test]
307    fn test_string_upper_lower() {
308        let (_, mut env) = make_env();
309        run("(require '[clojure.string :as str])", &mut env).unwrap();
310        assert_eq!(
311            run("(str/upper-case \"hello\")", &mut env).unwrap(),
312            Value::string("HELLO")
313        );
314        assert_eq!(
315            run("(str/lower-case \"WORLD\")", &mut env).unwrap(),
316            Value::string("world")
317        );
318    }
319
320    #[test]
321    fn test_string_trim() {
322        let (_, mut env) = make_env();
323        run("(require '[clojure.string :as str])", &mut env).unwrap();
324        assert_eq!(
325            run("(str/trim \"  hello  \")", &mut env).unwrap(),
326            Value::string("hello")
327        );
328        assert_eq!(
329            run("(str/triml \"  hi\")", &mut env).unwrap(),
330            Value::string("hi")
331        );
332        assert_eq!(
333            run("(str/trimr \"hi  \")", &mut env).unwrap(),
334            Value::string("hi")
335        );
336    }
337
338    #[test]
339    fn test_string_predicates() {
340        let (_, mut env) = make_env();
341        run("(require '[clojure.string :as str])", &mut env).unwrap();
342        assert_eq!(
343            run("(str/blank? \"  \")", &mut env).unwrap(),
344            Value::Bool(true)
345        );
346        assert_eq!(
347            run("(str/blank? \"x\")", &mut env).unwrap(),
348            Value::Bool(false)
349        );
350        assert_eq!(
351            run("(str/starts-with? \"hello\" \"hel\")", &mut env).unwrap(),
352            Value::Bool(true)
353        );
354        assert_eq!(
355            run("(str/ends-with? \"hello\" \"llo\")", &mut env).unwrap(),
356            Value::Bool(true)
357        );
358        assert_eq!(
359            run("(str/includes? \"hello\" \"ell\")", &mut env).unwrap(),
360            Value::Bool(true)
361        );
362    }
363
364    #[test]
365    fn test_string_replace() {
366        let (_, mut env) = make_env();
367        run("(require '[clojure.string :as str])", &mut env).unwrap();
368        assert_eq!(
369            run("(str/replace \"aabbcc\" \"bb\" \"XX\")", &mut env).unwrap(),
370            Value::string("aaXXcc")
371        );
372        assert_eq!(
373            run("(str/replace-first \"aabbcc\" \"a\" \"X\")", &mut env).unwrap(),
374            Value::string("Xabbcc")
375        );
376    }
377
378    #[test]
379    fn test_string_split_join() {
380        let (_, mut env) = make_env();
381        run("(require '[clojure.string :as str])", &mut env).unwrap();
382        let v = run("(str/split \"a,b,c\" \",\")", &mut env).unwrap();
383        assert!(matches!(v, Value::Vector(_)));
384        assert_eq!(
385            run("(str/join \"-\" [\"a\" \"b\" \"c\"])", &mut env).unwrap(),
386            Value::string("a-b-c")
387        );
388    }
389
390    #[test]
391    fn test_string_capitalize() {
392        let (_, mut env) = make_env();
393        run("(require '[clojure.string :as str])", &mut env).unwrap();
394        assert_eq!(
395            run("(str/capitalize \"hello world\")", &mut env).unwrap(),
396            Value::string("Hello world")
397        );
398    }
399
400    #[test]
401    fn test_string_split_lines() {
402        let (_, mut env) = make_env();
403        run("(require '[clojure.string :as str])", &mut env).unwrap();
404        let v = run("(str/split-lines \"a\\nb\\nc\")", &mut env).unwrap();
405        assert!(matches!(v, Value::Vector(_)));
406    }
407
408    // ── clojure.set ───────────────────────────────────────────────────────────
409
410    #[test]
411    fn test_set_union() {
412        let (_, mut env) = make_env();
413        run("(require '[clojure.set :as s])", &mut env).unwrap();
414        let v = run("(s/union #{1 2} #{2 3})", &mut env).unwrap();
415        match v {
416            Value::Set(s) => assert_eq!(s.count(), 3),
417            other => panic!("expected set, got {other:?}"),
418        }
419    }
420
421    #[test]
422    fn test_set_intersection() {
423        let (_, mut env) = make_env();
424        run("(require '[clojure.set :as s])", &mut env).unwrap();
425        let v = run("(s/intersection #{1 2 3} #{2 3 4})", &mut env).unwrap();
426        match v {
427            Value::Set(s) => assert_eq!(s.count(), 2),
428            other => panic!("expected set, got {other:?}"),
429        }
430    }
431
432    #[test]
433    fn test_set_difference() {
434        let (_, mut env) = make_env();
435        run("(require '[clojure.set :as s])", &mut env).unwrap();
436        let v = run("(s/difference #{1 2 3} #{2 3})", &mut env).unwrap();
437        match v {
438            Value::Set(s) => assert_eq!(s.count(), 1),
439            other => panic!("expected set, got {other:?}"),
440        }
441    }
442
443    #[test]
444    fn test_set_subset_superset() {
445        let (_, mut env) = make_env();
446        run("(require '[clojure.set :as s])", &mut env).unwrap();
447        assert_eq!(
448            run("(s/subset? #{1 2} #{1 2 3})", &mut env).unwrap(),
449            Value::Bool(true)
450        );
451        assert_eq!(
452            run("(s/superset? #{1 2 3} #{1 2})", &mut env).unwrap(),
453            Value::Bool(true)
454        );
455    }
456
457    #[test]
458    fn test_set_map_invert() {
459        let (_, mut env) = make_env();
460        run("(require '[clojure.set :as s])", &mut env).unwrap();
461        let v = run("(s/map-invert {:a 1 :b 2})", &mut env).unwrap();
462        assert!(matches!(v, Value::Map(_)));
463    }
464
465    // ── clojure.test (via stdlib registry) ───────────────────────────────────
466
467    #[test]
468    fn test_clojure_test_lazy_load() {
469        // Run on a thread with adequate stack: the `is` macro expansion
470        // triggers eager IR lowering, which calls the Clojure compiler
471        // (deeply recursive — needs more than the default 2MB test thread stack).
472        std::thread::Builder::new()
473            .stack_size(16 * 1024 * 1024)
474            .spawn(|| {
475                let (_, mut env) = make_env();
476                // clojure.test is NOT pre-loaded in standard_env_minimal();
477                // it should load lazily from the registry.
478                run(
479                    "(require '[clojure.test :refer [is deftest run-tests]])",
480                    &mut env,
481                )
482                .unwrap();
483                let v = run("(is (= 1 1))", &mut env).unwrap();
484                assert_eq!(v, Value::Bool(true));
485            })
486            .unwrap()
487            .join()
488            .unwrap();
489    }
490}