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