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/// Create a `GlobalEnv` with all built-ins and stdlib registered, **without**
106/// the IR lowering hook.
107///
108/// Use this in the AOT test harness and any other execution context where
109/// IR generation is not needed.  It avoids populating the global `IR_CACHE`
110/// with entries for test-namespace functions (entries that would never be
111/// evicted and would accumulate to hundreds of MB over 233 namespaces).
112///
113/// GC config and root tracer are still registered identically to `standard_env`.
114#[cfg(not(target_arch = "wasm32"))]
115pub fn standard_env_no_ir() -> Arc<GlobalEnv> {
116    let globals = cljrs_eval::standard_env_minimal_no_ir();
117    register(&globals);
118
119    cljrs_gc::HEAP.set_config_from_env();
120    let roots_gc = globals.clone();
121    cljrs_gc::HEAP.register_root_tracer(move |visitor| {
122        use cljrs_gc::GcVisitor as _;
123        let namespaces = roots_gc.namespaces.read().unwrap();
124        for (_name, ns_ptr) in namespaces.iter() {
125            visitor.visit(ns_ptr);
126        }
127    });
128
129    globals
130}
131
132/// Create a `GlobalEnv` with all built-ins and stdlib registered.
133///
134/// Prefer this over `cljrs_eval::standard_env()` in the `cljrs` binary so that
135/// stdlib namespaces are loaded lazily (only on first `require`) instead of
136/// eagerly at startup.
137#[cfg(not(target_arch = "wasm32"))]
138pub fn standard_env() -> Arc<GlobalEnv> {
139    let globals = cljrs_eval::standard_env_minimal();
140    register(&globals);
141
142    // Configure GC with default limits and register namespace bindings as roots.
143    // Without this, the GC never fires (no config → no soft-limit check).
144    // standard_env_with_paths_and_config() overrides the config but reuses this tracer.
145    cljrs_gc::HEAP.set_config_from_env();
146    let roots_gc = globals.clone();
147    cljrs_gc::HEAP.register_root_tracer(move |visitor| {
148        use cljrs_gc::GcVisitor as _;
149        let namespaces = roots_gc.namespaces.read().unwrap();
150        for (_name, ns_ptr) in namespaces.iter() {
151            visitor.visit(ns_ptr);
152        }
153    });
154
155    // Enable IR lowering (pure Rust — nothing to load; honors CLJRS_NO_IR).
156    cljrs_eval::mark_compiler_ready(&globals);
157
158    globals
159}
160
161/// Like [`standard_env()`] but also sets user source paths for `require`.
162#[cfg(not(target_arch = "wasm32"))]
163pub fn standard_env_with_paths(source_paths: Vec<std::path::PathBuf>) -> Arc<GlobalEnv> {
164    let globals = standard_env();
165    globals.set_source_paths(source_paths);
166    globals
167}
168
169/// Like [`standard_env_with_paths()`] but also sets GC configuration.
170#[cfg(not(target_arch = "wasm32"))]
171pub fn standard_env_with_paths_and_config(
172    source_paths: Vec<std::path::PathBuf>,
173    gc_config: Arc<GcConfig>,
174) -> Arc<GlobalEnv> {
175    let globals = standard_env();
176    globals.set_source_paths(source_paths);
177    globals.set_gc_config(gc_config.clone());
178    // Override the default GC config set by standard_env() with the custom limits.
179    // The root tracer is already registered by standard_env().
180    cljrs_gc::HEAP.set_config(gc_config);
181    globals
182}
183
184// ── Tests ─────────────────────────────────────────────────────────────────────
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use cljrs_eval::{Env, EvalResult, eval};
190    use cljrs_reader::Parser;
191    use cljrs_value::Value;
192
193    fn make_env() -> (Arc<GlobalEnv>, Env) {
194        let globals = standard_env();
195        let env = Env::new(globals.clone(), "user");
196        (globals, env)
197    }
198
199    #[allow(clippy::result_large_err)]
200    fn run(src: &str, env: &mut Env) -> EvalResult {
201        let mut parser = Parser::new(src.to_string(), "<test>".to_string());
202        let forms = parser.parse_all().expect("parse error");
203        let mut result = Value::Nil;
204        for form in forms {
205            result = eval(&form, env)?;
206        }
207        Ok(result)
208    }
209
210    // ── clojure.string ────────────────────────────────────────────────────────
211
212    #[test]
213    fn test_string_upper_lower() {
214        let (_, mut env) = make_env();
215        run("(require '[clojure.string :as str])", &mut env).unwrap();
216        assert_eq!(
217            run("(str/upper-case \"hello\")", &mut env).unwrap(),
218            Value::string("HELLO")
219        );
220        assert_eq!(
221            run("(str/lower-case \"WORLD\")", &mut env).unwrap(),
222            Value::string("world")
223        );
224    }
225
226    #[test]
227    fn test_string_trim() {
228        let (_, mut env) = make_env();
229        run("(require '[clojure.string :as str])", &mut env).unwrap();
230        assert_eq!(
231            run("(str/trim \"  hello  \")", &mut env).unwrap(),
232            Value::string("hello")
233        );
234        assert_eq!(
235            run("(str/triml \"  hi\")", &mut env).unwrap(),
236            Value::string("hi")
237        );
238        assert_eq!(
239            run("(str/trimr \"hi  \")", &mut env).unwrap(),
240            Value::string("hi")
241        );
242    }
243
244    #[test]
245    fn test_string_predicates() {
246        let (_, mut env) = make_env();
247        run("(require '[clojure.string :as str])", &mut env).unwrap();
248        assert_eq!(
249            run("(str/blank? \"  \")", &mut env).unwrap(),
250            Value::Bool(true)
251        );
252        assert_eq!(
253            run("(str/blank? \"x\")", &mut env).unwrap(),
254            Value::Bool(false)
255        );
256        assert_eq!(
257            run("(str/starts-with? \"hello\" \"hel\")", &mut env).unwrap(),
258            Value::Bool(true)
259        );
260        assert_eq!(
261            run("(str/ends-with? \"hello\" \"llo\")", &mut env).unwrap(),
262            Value::Bool(true)
263        );
264        assert_eq!(
265            run("(str/includes? \"hello\" \"ell\")", &mut env).unwrap(),
266            Value::Bool(true)
267        );
268    }
269
270    #[test]
271    fn test_string_replace() {
272        let (_, mut env) = make_env();
273        run("(require '[clojure.string :as str])", &mut env).unwrap();
274        assert_eq!(
275            run("(str/replace \"aabbcc\" \"bb\" \"XX\")", &mut env).unwrap(),
276            Value::string("aaXXcc")
277        );
278        assert_eq!(
279            run("(str/replace-first \"aabbcc\" \"a\" \"X\")", &mut env).unwrap(),
280            Value::string("Xabbcc")
281        );
282        // regex match (issue #188)
283        assert_eq!(
284            run("(str/replace \"--host\" #\"^--\" \"\")", &mut env).unwrap(),
285            Value::string("host")
286        );
287        assert_eq!(
288            run("(str/replace \"aaa\" #\"a\" \"b\")", &mut env).unwrap(),
289            Value::string("bbb")
290        );
291        assert_eq!(
292            run("(str/replace-first \"aaa\" #\"a\" \"b\")", &mut env).unwrap(),
293            Value::string("baa")
294        );
295        assert_eq!(
296            run("(str/replace-first \"--host\" #\"^--\" \"\")", &mut env).unwrap(),
297            Value::string("host")
298        );
299    }
300
301    #[test]
302    fn test_string_split_join() {
303        let (_, mut env) = make_env();
304        run("(require '[clojure.string :as str])", &mut env).unwrap();
305        let v = run("(str/split \"a,b,c\" \",\")", &mut env).unwrap();
306        assert!(matches!(v, Value::Vector(_)));
307        assert_eq!(
308            run("(str/join \"-\" [\"a\" \"b\" \"c\"])", &mut env).unwrap(),
309            Value::string("a-b-c")
310        );
311    }
312
313    #[test]
314    fn test_string_join_char_elements() {
315        let (_, mut env) = make_env();
316        run("(require '[clojure.string :as str])", &mut env).unwrap();
317        // Characters must render as their string value, not reader syntax.
318        assert_eq!(
319            run(r"(str/join [\8 \0])", &mut env).unwrap(),
320            Value::string("80")
321        );
322        assert_eq!(
323            run(r"(str/join \- [\8 \0])", &mut env).unwrap(),
324            Value::string("8-0")
325        );
326        // nil elements are treated as empty string (same as (str nil) = "").
327        assert_eq!(
328            run(r#"(str/join "-" [nil "a" nil])"#, &mut env).unwrap(),
329            Value::string("-a-")
330        );
331    }
332
333    #[test]
334    fn test_string_capitalize() {
335        let (_, mut env) = make_env();
336        run("(require '[clojure.string :as str])", &mut env).unwrap();
337        assert_eq!(
338            run("(str/capitalize \"hello world\")", &mut env).unwrap(),
339            Value::string("Hello world")
340        );
341    }
342
343    #[test]
344    fn test_string_split_lines() {
345        let (_, mut env) = make_env();
346        run("(require '[clojure.string :as str])", &mut env).unwrap();
347        let v = run("(str/split-lines \"a\\nb\\nc\")", &mut env).unwrap();
348        assert!(matches!(v, Value::Vector(_)));
349    }
350
351    // ── clojure.set ───────────────────────────────────────────────────────────
352
353    #[test]
354    fn test_set_union() {
355        let (_, mut env) = make_env();
356        run("(require '[clojure.set :as s])", &mut env).unwrap();
357        let v = run("(s/union #{1 2} #{2 3})", &mut env).unwrap();
358        match v {
359            Value::Set(s) => assert_eq!(s.count(), 3),
360            other => panic!("expected set, got {other:?}"),
361        }
362    }
363
364    #[test]
365    fn test_set_intersection() {
366        let (_, mut env) = make_env();
367        run("(require '[clojure.set :as s])", &mut env).unwrap();
368        let v = run("(s/intersection #{1 2 3} #{2 3 4})", &mut env).unwrap();
369        match v {
370            Value::Set(s) => assert_eq!(s.count(), 2),
371            other => panic!("expected set, got {other:?}"),
372        }
373    }
374
375    #[test]
376    fn test_set_difference() {
377        let (_, mut env) = make_env();
378        run("(require '[clojure.set :as s])", &mut env).unwrap();
379        let v = run("(s/difference #{1 2 3} #{2 3})", &mut env).unwrap();
380        match v {
381            Value::Set(s) => assert_eq!(s.count(), 1),
382            other => panic!("expected set, got {other:?}"),
383        }
384    }
385
386    #[test]
387    fn test_set_subset_superset() {
388        let (_, mut env) = make_env();
389        run("(require '[clojure.set :as s])", &mut env).unwrap();
390        assert_eq!(
391            run("(s/subset? #{1 2} #{1 2 3})", &mut env).unwrap(),
392            Value::Bool(true)
393        );
394        assert_eq!(
395            run("(s/superset? #{1 2 3} #{1 2})", &mut env).unwrap(),
396            Value::Bool(true)
397        );
398    }
399
400    #[test]
401    fn test_set_map_invert() {
402        let (_, mut env) = make_env();
403        run("(require '[clojure.set :as s])", &mut env).unwrap();
404        let v = run("(s/map-invert {:a 1 :b 2})", &mut env).unwrap();
405        assert!(matches!(v, Value::Map(_)));
406    }
407
408    // ── clojure.test (via stdlib registry) ───────────────────────────────────
409
410    #[test]
411    fn test_clojure_test_lazy_load() {
412        // Run on a thread with adequate stack: the `is` macro expansion
413        // triggers eager IR lowering, which calls the Clojure compiler
414        // (deeply recursive — needs more than the default 2MB test thread stack).
415        std::thread::Builder::new()
416            .stack_size(16 * 1024 * 1024)
417            .spawn(|| {
418                let (_, mut env) = make_env();
419                // clojure.test is NOT pre-loaded in standard_env_minimal();
420                // it should load lazily from the registry.
421                run(
422                    "(require '[clojure.test :refer [is deftest run-tests]])",
423                    &mut env,
424                )
425                .unwrap();
426                let v = run("(is (= 1 1))", &mut env).unwrap();
427                assert_eq!(v, Value::Bool(true));
428            })
429            .unwrap()
430            .join()
431            .unwrap();
432    }
433}