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/// Create a `GlobalEnv` with all built-ins and stdlib registered.
98///
99/// Prefer this over `cljrs_eval::standard_env()` in the `cljrs` binary so that
100/// stdlib namespaces are loaded lazily (only on first `require`) instead of
101/// eagerly at startup.
102pub fn standard_env() -> Arc<GlobalEnv> {
103    let globals = cljrs_eval::standard_env_minimal();
104    register(&globals);
105    globals
106}
107
108/// Like [`standard_env()`] but also sets user source paths for `require`.
109pub fn standard_env_with_paths(source_paths: Vec<std::path::PathBuf>) -> Arc<GlobalEnv> {
110    let globals = standard_env();
111    globals.set_source_paths(source_paths);
112    globals
113}
114
115/// Like [`standard_env_with_paths()`] but also sets GC configuration.
116pub fn standard_env_with_paths_and_config(
117    source_paths: Vec<std::path::PathBuf>,
118    gc_config: Arc<GcConfig>,
119) -> Arc<GlobalEnv> {
120    let globals = standard_env();
121    globals.set_source_paths(source_paths);
122    globals.set_gc_config(gc_config.clone());
123    // Configure the global GC heap with the same limits
124    cljrs_gc::HEAP.set_config(gc_config);
125    // Register GlobalEnv namespaces as GC roots so automatic collection
126    // can trace all live values reachable from namespace bindings.
127    let roots_globals = globals.clone();
128    cljrs_gc::HEAP.register_root_tracer(move |visitor| {
129        use cljrs_gc::GcVisitor as _;
130        let namespaces = roots_globals.namespaces.read().unwrap();
131        for (_name, ns_ptr) in namespaces.iter() {
132            visitor.visit(ns_ptr);
133        }
134    });
135    globals
136}
137
138// ── Tests ─────────────────────────────────────────────────────────────────────
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use cljrs_eval::{Env, EvalResult, eval};
144    use cljrs_reader::Parser;
145    use cljrs_value::Value;
146
147    fn make_env() -> (Arc<GlobalEnv>, Env) {
148        let globals = standard_env();
149        let env = Env::new(globals.clone(), "user");
150        (globals, env)
151    }
152
153    fn run(src: &str, env: &mut Env) -> EvalResult {
154        let mut parser = Parser::new(src.to_string(), "<test>".to_string());
155        let forms = parser.parse_all().expect("parse error");
156        let mut result = Value::Nil;
157        for form in forms {
158            result = eval(&form, env)?;
159        }
160        Ok(result)
161    }
162
163    // ── clojure.string ────────────────────────────────────────────────────────
164
165    #[test]
166    fn test_string_upper_lower() {
167        let (_, mut env) = make_env();
168        run("(require '[clojure.string :as str])", &mut env).unwrap();
169        assert_eq!(
170            run("(str/upper-case \"hello\")", &mut env).unwrap(),
171            Value::string("HELLO")
172        );
173        assert_eq!(
174            run("(str/lower-case \"WORLD\")", &mut env).unwrap(),
175            Value::string("world")
176        );
177    }
178
179    #[test]
180    fn test_string_trim() {
181        let (_, mut env) = make_env();
182        run("(require '[clojure.string :as str])", &mut env).unwrap();
183        assert_eq!(
184            run("(str/trim \"  hello  \")", &mut env).unwrap(),
185            Value::string("hello")
186        );
187        assert_eq!(
188            run("(str/triml \"  hi\")", &mut env).unwrap(),
189            Value::string("hi")
190        );
191        assert_eq!(
192            run("(str/trimr \"hi  \")", &mut env).unwrap(),
193            Value::string("hi")
194        );
195    }
196
197    #[test]
198    fn test_string_predicates() {
199        let (_, mut env) = make_env();
200        run("(require '[clojure.string :as str])", &mut env).unwrap();
201        assert_eq!(
202            run("(str/blank? \"  \")", &mut env).unwrap(),
203            Value::Bool(true)
204        );
205        assert_eq!(
206            run("(str/blank? \"x\")", &mut env).unwrap(),
207            Value::Bool(false)
208        );
209        assert_eq!(
210            run("(str/starts-with? \"hello\" \"hel\")", &mut env).unwrap(),
211            Value::Bool(true)
212        );
213        assert_eq!(
214            run("(str/ends-with? \"hello\" \"llo\")", &mut env).unwrap(),
215            Value::Bool(true)
216        );
217        assert_eq!(
218            run("(str/includes? \"hello\" \"ell\")", &mut env).unwrap(),
219            Value::Bool(true)
220        );
221    }
222
223    #[test]
224    fn test_string_replace() {
225        let (_, mut env) = make_env();
226        run("(require '[clojure.string :as str])", &mut env).unwrap();
227        assert_eq!(
228            run("(str/replace \"aabbcc\" \"bb\" \"XX\")", &mut env).unwrap(),
229            Value::string("aaXXcc")
230        );
231        assert_eq!(
232            run("(str/replace-first \"aabbcc\" \"a\" \"X\")", &mut env).unwrap(),
233            Value::string("Xabbcc")
234        );
235    }
236
237    #[test]
238    fn test_string_split_join() {
239        let (_, mut env) = make_env();
240        run("(require '[clojure.string :as str])", &mut env).unwrap();
241        let v = run("(str/split \"a,b,c\" \",\")", &mut env).unwrap();
242        assert!(matches!(v, Value::Vector(_)));
243        assert_eq!(
244            run("(str/join \"-\" [\"a\" \"b\" \"c\"])", &mut env).unwrap(),
245            Value::string("a-b-c")
246        );
247    }
248
249    #[test]
250    fn test_string_capitalize() {
251        let (_, mut env) = make_env();
252        run("(require '[clojure.string :as str])", &mut env).unwrap();
253        assert_eq!(
254            run("(str/capitalize \"hello world\")", &mut env).unwrap(),
255            Value::string("Hello world")
256        );
257    }
258
259    #[test]
260    fn test_string_split_lines() {
261        let (_, mut env) = make_env();
262        run("(require '[clojure.string :as str])", &mut env).unwrap();
263        let v = run("(str/split-lines \"a\\nb\\nc\")", &mut env).unwrap();
264        assert!(matches!(v, Value::Vector(_)));
265    }
266
267    // ── clojure.set ───────────────────────────────────────────────────────────
268
269    #[test]
270    fn test_set_union() {
271        let (_, mut env) = make_env();
272        run("(require '[clojure.set :as s])", &mut env).unwrap();
273        let v = run("(s/union #{1 2} #{2 3})", &mut env).unwrap();
274        match v {
275            Value::Set(s) => assert_eq!(s.count(), 3),
276            other => panic!("expected set, got {other:?}"),
277        }
278    }
279
280    #[test]
281    fn test_set_intersection() {
282        let (_, mut env) = make_env();
283        run("(require '[clojure.set :as s])", &mut env).unwrap();
284        let v = run("(s/intersection #{1 2 3} #{2 3 4})", &mut env).unwrap();
285        match v {
286            Value::Set(s) => assert_eq!(s.count(), 2),
287            other => panic!("expected set, got {other:?}"),
288        }
289    }
290
291    #[test]
292    fn test_set_difference() {
293        let (_, mut env) = make_env();
294        run("(require '[clojure.set :as s])", &mut env).unwrap();
295        let v = run("(s/difference #{1 2 3} #{2 3})", &mut env).unwrap();
296        match v {
297            Value::Set(s) => assert_eq!(s.count(), 1),
298            other => panic!("expected set, got {other:?}"),
299        }
300    }
301
302    #[test]
303    fn test_set_subset_superset() {
304        let (_, mut env) = make_env();
305        run("(require '[clojure.set :as s])", &mut env).unwrap();
306        assert_eq!(
307            run("(s/subset? #{1 2} #{1 2 3})", &mut env).unwrap(),
308            Value::Bool(true)
309        );
310        assert_eq!(
311            run("(s/superset? #{1 2 3} #{1 2})", &mut env).unwrap(),
312            Value::Bool(true)
313        );
314    }
315
316    #[test]
317    fn test_set_map_invert() {
318        let (_, mut env) = make_env();
319        run("(require '[clojure.set :as s])", &mut env).unwrap();
320        let v = run("(s/map-invert {:a 1 :b 2})", &mut env).unwrap();
321        assert!(matches!(v, Value::Map(_)));
322    }
323
324    // ── clojure.test (via stdlib registry) ───────────────────────────────────
325
326    #[test]
327    fn test_clojure_test_lazy_load() {
328        let (_, mut env) = make_env();
329        // clojure.test is NOT pre-loaded in standard_env_minimal();
330        // it should load lazily from the registry.
331        run(
332            "(require '[clojure.test :refer [is deftest run-tests]])",
333            &mut env,
334        )
335        .unwrap();
336        let v = run("(is (= 1 1))", &mut env).unwrap();
337        assert_eq!(v, Value::Bool(true));
338    }
339}