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//! runtime so they are available via `(require ...)` without needing source
5//! files on disk.
6//!
7//! ## Entry point
8//!
9//! ```no_run
10//! use cljrs_runtime::{ExecutionMode, Runtime};
11//!
12//! let runtime = Runtime::builder()
13//!     .execution_mode(ExecutionMode::Tiered)
14//!     .build()
15//!     .expect("bootstrap");
16//! cljrs_stdlib::install(&runtime);
17//! ```
18//!
19//! This package is an *extension*: it does not construct runtimes and does not
20//! decide execution modes.  [`install`] adds namespaces to a runtime the
21//! caller already built — the CLI, an embedding host, or the AOT compiler
22//! picks the mode, source paths, and GC limits.
23//!
24//! [`register`] is the same thing addressed by [`GlobalEnv`], for callers that
25//! only hold an environment handle.
26
27use std::sync::Arc;
28
29use cljrs_runtime::Runtime;
30use cljrs_runtime::env::env::GlobalEnv;
31
32// io and edn use std::fs which is not available on wasm32-unknown-unknown
33#[cfg(not(target_arch = "wasm32"))]
34mod edn;
35#[cfg(not(target_arch = "wasm32"))]
36pub mod io;
37#[cfg(not(target_arch = "wasm32"))]
38pub mod process;
39mod set;
40mod string;
41// ── Embedded sources ──────────────────────────────────────────────────────────
42
43const CLOJURE_TEST_SRC: &str = include_str!("clojure/test.cljrs");
44const CLOJURE_STRING_SRC: &str = include_str!("clojure/string.cljrs");
45const CLOJURE_SET_SRC: &str = include_str!("clojure/set.cljrs");
46const CLOJURE_TEMPLATE_SRC: &str = include_str!("clojure/template.cljrs");
47#[cfg(not(target_arch = "wasm32"))]
48const CLOJURE_RUST_IO_SRC: &str = include_str!("clojure/rust/io.cljrs");
49#[cfg(not(target_arch = "wasm32"))]
50const CLOJURE_EDN_SRC: &str = include_str!("clojure/edn.cljrs");
51#[cfg(not(target_arch = "wasm32"))]
52const CLOJURE_JAVA_SHELL_SRC: &str = include_str!("clojure/java/shell.cljrs");
53const CLOJURE_WALK_SRC: &str = include_str!("clojure/walk.cljrs");
54const CLOJURE_PPRINT_SRC: &str = include_str!("clojure/pprint.cljrs");
55const CLOJURE_DATA_SRC: &str = include_str!("clojure/data.cljrs");
56const COLJURE_ZIP_SRC: &str = include_str!("clojure/zip.cljrs");
57const CLOJURE_SPEC_ALPHA_SRC: &str = include_str!("clojure/spec/alpha.cljrs");
58const CLOJURE_SPEC_GEN_ALPHA_SRC: &str = include_str!("clojure/spec/gen/alpha.cljrs");
59const CLOJURE_SPEC_TEST_ALPHA_SRC: &str = include_str!("clojure/spec/test/alpha.cljrs");
60
61// ── Macro: register a batch of native fns into a namespace ───────────────────
62
63/// Register a slice of `(name, arity, fn)` triples as `NativeFunction` values
64/// in `$globals` under namespace `$ns`.
65macro_rules! register_fns {
66    ($globals:expr, $ns:expr, [ $( ($name:expr, $arity:expr, $func:expr) ),* $(,)? ]) => {{
67        use cljrs_gc::GcPtr;
68        use cljrs_value::{NativeFn, Value};
69        let ns: &str = $ns;
70        $(
71            {
72                let nf = NativeFn::new($name, $arity, $func);
73                $globals.intern(ns, std::sync::Arc::from($name), Value::NativeFunction(GcPtr::new(nf)));
74            }
75        )*
76    }};
77}
78
79pub(crate) use register_fns;
80
81// ── Public API ────────────────────────────────────────────────────────────────
82
83/// Register all built-in stdlib namespaces into `globals`.
84///
85/// This is idempotent: calling it again does not re-evaluate sources
86/// (already-loaded guard in `load_ns` prevents that), but it will
87/// overwrite native fn registrations in the namespace tables.
88/// In practice, call it once right after `standard_env_minimal()`.
89pub fn register(globals: &Arc<GlobalEnv>) {
90    // clojure.string ─ pre-register native fns, then register source for
91    // the lazy (ns clojure.string) form to run on first require.
92    string::register(globals, "clojure.string");
93    globals.register_builtin_source("clojure.string", CLOJURE_STRING_SRC);
94
95    // clojure.set ─ same pattern.
96    set::register(globals, "clojure.set");
97    globals.register_builtin_source("clojure.set", CLOJURE_SET_SRC);
98
99    // clojure.template ─ pure Clojure, no native helpers.
100    globals.register_builtin_source("clojure.template", CLOJURE_TEMPLATE_SRC);
101
102    // clojure.test ─ pure Clojure, no native helpers.
103    globals.register_builtin_source("clojure.test", CLOJURE_TEST_SRC);
104
105    // clojure.rust.io and clojure.edn use std::fs, unavailable on wasm32.
106    #[cfg(not(target_arch = "wasm32"))]
107    {
108        io::register(globals, "clojure.rust.io");
109        globals.register_builtin_source("clojure.rust.io", CLOJURE_RUST_IO_SRC);
110
111        edn::register(globals, "clojure.edn");
112        globals.register_builtin_source("clojure.edn", CLOJURE_EDN_SRC);
113
114        // clojure.java.shell ─ `sh` over the native clojure.rust.process/run,
115        // which is also a transaction-policy denied capability.
116        process::register(globals, "clojure.rust.process");
117        globals.register_builtin_source("clojure.java.shell", CLOJURE_JAVA_SHELL_SRC);
118    }
119
120    // clojure.walk ─ pure Clojure, no native helpers.
121    globals.register_builtin_source("clojure.walk", CLOJURE_WALK_SRC);
122
123    // clojure.data ─ pure Clojure, no native helpers.
124    globals.register_builtin_source("clojure.data", CLOJURE_DATA_SRC);
125
126    // clojure.pprint ─ pure Clojure; data dispatch only, no cl-format.
127    globals.register_builtin_source("clojure.pprint", CLOJURE_PPRINT_SRC);
128
129    // clojure.zip
130    globals.register_builtin_source("clojure.zip", COLJURE_ZIP_SRC);
131
132    // clojure.spec.alpha ─ pure Clojure, no native helpers.
133    globals.register_builtin_source("clojure.spec.alpha", CLOJURE_SPEC_ALPHA_SRC);
134
135    // clojure.spec.gen.alpha ─ pure Clojure, throwing generator stubs.
136    globals.register_builtin_source("clojure.spec.gen.alpha", CLOJURE_SPEC_GEN_ALPHA_SRC);
137
138    // clojure.spec.test.alpha ─ pure Clojure, instrument/unstrument.
139    globals.register_builtin_source("clojure.spec.test.alpha", CLOJURE_SPEC_TEST_ALPHA_SRC);
140}
141
142/// Install every built-in stdlib namespace into `runtime`.
143///
144/// Idempotent: calling it again does not re-evaluate sources (`load_ns`'s
145/// already-loaded guard prevents that), but it does overwrite the native fn
146/// registrations in each namespace.
147///
148/// Namespaces are registered as embedded *sources*, so each one is parsed and
149/// evaluated on its first `require` rather than at install time.
150pub fn install(runtime: &Runtime) {
151    register(runtime.globals());
152}
153
154// ── Tests ─────────────────────────────────────────────────────────────────────
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use cljrs_reader::Parser;
160    use cljrs_runtime::tiered::{Env, EvalResult, eval};
161    use cljrs_runtime::{ExecutionMode, Runtime};
162    use cljrs_value::{Keyword, Value};
163
164    fn make_env() -> (Arc<GlobalEnv>, Env) {
165        let runtime = Runtime::builder()
166            .execution_mode(ExecutionMode::Tiered)
167            .build()
168            .expect("runtime");
169        install(&runtime);
170        let globals = runtime.globals().clone();
171        let env = Env::new(globals.clone(), "user");
172        (globals, env)
173    }
174
175    #[allow(clippy::result_large_err)]
176    fn run(src: &str, env: &mut Env) -> EvalResult {
177        let mut parser = Parser::new(src.to_string(), "<test>".to_string());
178        let forms = parser.parse_all().expect("parse error");
179        let mut result = Value::Nil;
180        for form in forms {
181            result = eval(&form, env)?;
182        }
183        Ok(result)
184    }
185
186    // ── clojure.string ────────────────────────────────────────────────────────
187
188    #[test]
189    fn test_string_upper_lower() {
190        let (_, mut env) = make_env();
191        run("(require '[clojure.string :as str])", &mut env).unwrap();
192        assert_eq!(
193            run("(str/upper-case \"hello\")", &mut env).unwrap(),
194            Value::string("HELLO")
195        );
196        assert_eq!(
197            run("(str/lower-case \"WORLD\")", &mut env).unwrap(),
198            Value::string("world")
199        );
200    }
201
202    #[test]
203    fn test_string_trim() {
204        let (_, mut env) = make_env();
205        run("(require '[clojure.string :as str])", &mut env).unwrap();
206        assert_eq!(
207            run("(str/trim \"  hello  \")", &mut env).unwrap(),
208            Value::string("hello")
209        );
210        assert_eq!(
211            run("(str/triml \"  hi\")", &mut env).unwrap(),
212            Value::string("hi")
213        );
214        assert_eq!(
215            run("(str/trimr \"hi  \")", &mut env).unwrap(),
216            Value::string("hi")
217        );
218    }
219
220    #[test]
221    fn test_string_predicates() {
222        let (_, mut env) = make_env();
223        run("(require '[clojure.string :as str])", &mut env).unwrap();
224        assert_eq!(
225            run("(str/blank? \"  \")", &mut env).unwrap(),
226            Value::Bool(true)
227        );
228        assert_eq!(
229            run("(str/blank? \"x\")", &mut env).unwrap(),
230            Value::Bool(false)
231        );
232        assert_eq!(
233            run("(str/starts-with? \"hello\" \"hel\")", &mut env).unwrap(),
234            Value::Bool(true)
235        );
236        assert_eq!(
237            run("(str/ends-with? \"hello\" \"llo\")", &mut env).unwrap(),
238            Value::Bool(true)
239        );
240        assert_eq!(
241            run("(str/includes? \"hello\" \"ell\")", &mut env).unwrap(),
242            Value::Bool(true)
243        );
244    }
245
246    #[test]
247    fn test_string_replace() {
248        let (_, mut env) = make_env();
249        run("(require '[clojure.string :as str])", &mut env).unwrap();
250        assert_eq!(
251            run("(str/replace \"aabbcc\" \"bb\" \"XX\")", &mut env).unwrap(),
252            Value::string("aaXXcc")
253        );
254        assert_eq!(
255            run("(str/replace-first \"aabbcc\" \"a\" \"X\")", &mut env).unwrap(),
256            Value::string("Xabbcc")
257        );
258        // regex match (issue #188)
259        assert_eq!(
260            run("(str/replace \"--host\" #\"^--\" \"\")", &mut env).unwrap(),
261            Value::string("host")
262        );
263        assert_eq!(
264            run("(str/replace \"aaa\" #\"a\" \"b\")", &mut env).unwrap(),
265            Value::string("bbb")
266        );
267        assert_eq!(
268            run("(str/replace-first \"aaa\" #\"a\" \"b\")", &mut env).unwrap(),
269            Value::string("baa")
270        );
271        assert_eq!(
272            run("(str/replace-first \"--host\" #\"^--\" \"\")", &mut env).unwrap(),
273            Value::string("host")
274        );
275    }
276
277    #[test]
278    fn test_string_split_join() {
279        let (_, mut env) = make_env();
280        run("(require '[clojure.string :as str])", &mut env).unwrap();
281        let v = run("(str/split \"a,b,c\" \",\")", &mut env).unwrap();
282        assert!(matches!(v, Value::Vector(_)));
283        assert_eq!(
284            run("(str/join \"-\" [\"a\" \"b\" \"c\"])", &mut env).unwrap(),
285            Value::string("a-b-c")
286        );
287    }
288
289    #[test]
290    fn test_string_join_char_elements() {
291        let (_, mut env) = make_env();
292        run("(require '[clojure.string :as str])", &mut env).unwrap();
293        // Characters must render as their string value, not reader syntax.
294        assert_eq!(
295            run(r"(str/join [\8 \0])", &mut env).unwrap(),
296            Value::string("80")
297        );
298        assert_eq!(
299            run(r"(str/join \- [\8 \0])", &mut env).unwrap(),
300            Value::string("8-0")
301        );
302        // nil elements are treated as empty string (same as (str nil) = "").
303        assert_eq!(
304            run(r#"(str/join "-" [nil "a" nil])"#, &mut env).unwrap(),
305            Value::string("-a-")
306        );
307    }
308
309    #[test]
310    fn test_string_capitalize() {
311        let (_, mut env) = make_env();
312        run("(require '[clojure.string :as str])", &mut env).unwrap();
313        assert_eq!(
314            run("(str/capitalize \"hello world\")", &mut env).unwrap(),
315            Value::string("Hello world")
316        );
317    }
318
319    #[test]
320    fn test_string_split_lines() {
321        let (_, mut env) = make_env();
322        run("(require '[clojure.string :as str])", &mut env).unwrap();
323        for (form, expected) in [
324            ("(str/split-lines \"\")", "[\"\"]"),
325            ("(str/split-lines \"\\n\\n\")", "[]"),
326            ("(str/split-lines \"a\\nb\\nc\")", "[\"a\" \"b\" \"c\"]"),
327            ("(str/split-lines \"a\\r\\nb\")", "[\"a\" \"b\"]"),
328            ("(str/split-lines \"a\\n\\nb\")", "[\"a\" \"\" \"b\"]"),
329            ("(str/split-lines \"a\\n\\n\")", "[\"a\"]"),
330        ] {
331            let value = run(form, &mut env).unwrap();
332            assert_eq!(value.to_string(), expected, "{form}");
333        }
334    }
335
336    #[test]
337    fn test_string_split_lines_rejects_non_strings() {
338        let (_, mut env) = make_env();
339        run("(require '[clojure.string :as str])", &mut env).unwrap();
340        for form in [
341            "(str/split-lines nil)",
342            "(str/split-lines \\A)",
343            "(str/split-lines 0)",
344            "(str/split-lines 0.0)",
345            "(str/split-lines :foo)",
346            "(str/split-lines 'foo)",
347            "(str/split-lines [])",
348        ] {
349            assert!(run(form, &mut env).is_err(), "{form} should fail");
350        }
351    }
352
353    // ── clojure.set ───────────────────────────────────────────────────────────
354
355    #[test]
356    fn test_set_union() {
357        let (_, mut env) = make_env();
358        run("(require '[clojure.set :as s])", &mut env).unwrap();
359        let v = run("(s/union #{1 2} #{2 3})", &mut env).unwrap();
360        match v {
361            Value::Set(s) => assert_eq!(s.count(), 3),
362            other => panic!("expected set, got {other:?}"),
363        }
364    }
365
366    #[test]
367    fn test_set_intersection() {
368        let (_, mut env) = make_env();
369        run("(require '[clojure.set :as s])", &mut env).unwrap();
370        let v = run("(s/intersection #{1 2 3} #{2 3 4})", &mut env).unwrap();
371        match v {
372            Value::Set(s) => assert_eq!(s.count(), 2),
373            other => panic!("expected set, got {other:?}"),
374        }
375    }
376
377    #[test]
378    fn test_set_difference() {
379        let (_, mut env) = make_env();
380        run("(require '[clojure.set :as s])", &mut env).unwrap();
381        let v = run("(s/difference #{1 2 3} #{2 3})", &mut env).unwrap();
382        match v {
383            Value::Set(s) => assert_eq!(s.count(), 1),
384            other => panic!("expected set, got {other:?}"),
385        }
386    }
387
388    #[test]
389    fn test_set_subset_superset() {
390        let (_, mut env) = make_env();
391        run("(require '[clojure.set :as s])", &mut env).unwrap();
392        assert_eq!(
393            run("(s/subset? #{1 2} #{1 2 3})", &mut env).unwrap(),
394            Value::Bool(true)
395        );
396        assert_eq!(
397            run("(s/superset? #{1 2 3} #{1 2})", &mut env).unwrap(),
398            Value::Bool(true)
399        );
400    }
401
402    #[test]
403    fn test_set_map_invert() {
404        let (_, mut env) = make_env();
405        run("(require '[clojure.set :as s])", &mut env).unwrap();
406        let v = run("(s/map-invert {:a 1 :b 2})", &mut env).unwrap();
407        assert!(matches!(v, Value::Map(_)));
408    }
409
410    // ── clojure.test (via stdlib registry) ───────────────────────────────────
411
412    #[test]
413    fn test_clojure_test_lazy_load() {
414        // Run on a thread with adequate stack: the `is` macro expansion
415        // triggers eager IR lowering, which calls the Clojure compiler
416        // (deeply recursive — needs more than the default 2MB test thread stack).
417        std::thread::Builder::new()
418            .stack_size(16 * 1024 * 1024)
419            .spawn(|| {
420                let (_, mut env) = make_env();
421                // clojure.test is NOT pre-loaded by the builder; it should
422                // load lazily from the registry `install` populated.
423                run(
424                    "(require '[clojure.test :refer [is deftest run-tests]])",
425                    &mut env,
426                )
427                .unwrap();
428                let v = run("(is (= 1 1))", &mut env).unwrap();
429                assert_eq!(v, Value::Bool(true));
430            })
431            .unwrap()
432            .join()
433            .unwrap();
434    }
435
436    // ── clojure.spec.alpha (M1: skeleton + predicate specs + and/or) ─────────
437
438    /// Runs `body` on a thread with a 16MB stack (macro-heavy `spec` loading
439    /// triggers eager IR lowering, which recurses deeply — see
440    /// `test_clojure_test_lazy_load` above for the same pattern).
441    fn run_with_big_stack<F: FnOnce() + Send + 'static>(body: F) {
442        std::thread::Builder::new()
443            .stack_size(16 * 1024 * 1024)
444            .spawn(body)
445            .unwrap()
446            .join()
447            .unwrap();
448    }
449
450    #[test]
451    fn test_spec_def_and_valid_conform() {
452        run_with_big_stack(|| {
453            let (_, mut env) = make_env();
454            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
455            assert_eq!(
456                run("(s/def ::x even?)", &mut env).unwrap(),
457                Value::keyword(Keyword::qualified("user", "x"))
458            );
459            assert_eq!(
460                run("(s/valid? ::x 4)", &mut env).unwrap(),
461                Value::Bool(true)
462            );
463            assert_eq!(
464                run("(s/valid? ::x 3)", &mut env).unwrap(),
465                Value::Bool(false)
466            );
467            assert_eq!(run("(s/conform ::x 4)", &mut env).unwrap(), Value::Long(4));
468            assert_eq!(
469                run("(s/invalid? (s/conform ::x 3))", &mut env).unwrap(),
470                Value::Bool(true)
471            );
472        });
473    }
474
475    #[test]
476    fn test_spec_set_and_keyword_ref() {
477        run_with_big_stack(|| {
478            let (_, mut env) = make_env();
479            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
480            run("(s/def ::color #{:red :green})", &mut env).unwrap();
481            assert_eq!(
482                run("(s/valid? ::color :red)", &mut env).unwrap(),
483                Value::Bool(true)
484            );
485            assert_eq!(
486                run("(s/valid? ::color :blue)", &mut env).unwrap(),
487                Value::Bool(false)
488            );
489
490            // keyword-registry-ref spec: ::y re-resolves ::x live.
491            run("(s/def ::x even?)", &mut env).unwrap();
492            run("(s/def ::y ::x)", &mut env).unwrap();
493            assert_eq!(
494                run("(s/valid? ::y 4)", &mut env).unwrap(),
495                Value::Bool(true)
496            );
497            assert_eq!(
498                run("(s/valid? ::y 3)", &mut env).unwrap(),
499                Value::Bool(false)
500            );
501        });
502    }
503
504    #[test]
505    fn test_spec_forward_reference() {
506        run_with_big_stack(|| {
507            let (_, mut env) = make_env();
508            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
509            // ::a refers to ::b before ::b is defined.
510            run("(s/def ::a ::b)", &mut env).unwrap();
511            run("(s/def ::b string?)", &mut env).unwrap();
512            assert_eq!(
513                run(r#"(s/conform ::a "hi")"#, &mut env).unwrap(),
514                Value::string("hi")
515            );
516            assert_eq!(
517                run("(s/invalid? (s/conform ::a 5))", &mut env).unwrap(),
518                Value::Bool(true)
519            );
520        });
521    }
522
523    #[test]
524    fn test_spec_and_or() {
525        run_with_big_stack(|| {
526            let (_, mut env) = make_env();
527            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
528            assert_eq!(
529                run("(s/valid? (s/and int? even?) 4)", &mut env).unwrap(),
530                Value::Bool(true)
531            );
532            assert_eq!(
533                run("(s/valid? (s/and int? even?) 3)", &mut env).unwrap(),
534                Value::Bool(false)
535            );
536            let v = run("(s/conform (s/or :i int? :s string?) 5)", &mut env).unwrap();
537            match v {
538                Value::Vector(vec) => {
539                    let items = vec.get().iter().cloned().collect::<Vec<_>>();
540                    assert_eq!(items.len(), 2);
541                    assert_eq!(items[0], Value::keyword(Keyword::simple("i")));
542                    assert_eq!(items[1], Value::Long(5));
543                }
544                other => panic!("expected [:i 5], got {other:?}"),
545            }
546        });
547    }
548
549    #[test]
550    fn test_spec_spec_and_registry_introspection() {
551        run_with_big_stack(|| {
552            let (_, mut env) = make_env();
553            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
554            // Bare predicates are directly usable but not `spec?`; our own
555            // record-based compound specs are.
556            assert_eq!(run("(s/spec? even?)", &mut env).unwrap(), Value::Nil);
557            let is_spec = run("(s/spec? (s/and int? even?))", &mut env).unwrap();
558            assert!(
559                !matches!(is_spec, Value::Nil),
560                "expected a truthy spec object"
561            );
562
563            run("(s/def ::x even?)", &mut env).unwrap();
564            assert!(!matches!(
565                run("(s/get-spec ::x)", &mut env).unwrap(),
566                Value::Nil
567            ));
568            assert!(matches!(
569                run("(s/get-spec ::does-not-exist)", &mut env).unwrap(),
570                Value::Nil
571            ));
572
573            run("(s/def ::pos-even (s/and int? even? pos?))", &mut env).unwrap();
574            let form_v = run("(s/form ::pos-even)", &mut env).unwrap();
575            assert_eq!(format!("{form_v}"), "(and int? even? pos?)");
576            let describe_v = run("(s/describe ::pos-even)", &mut env).unwrap();
577            assert_eq!(format!("{describe_v}"), "(and int? even? pos?)");
578        });
579    }
580
581    #[test]
582    fn test_spec_conform_unregistered_keyword_throws() {
583        run_with_big_stack(|| {
584            let (_, mut env) = make_env();
585            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
586            let err = run("(s/conform ::does-not-exist 1)", &mut env).unwrap_err();
587            let msg = format!("{err}");
588            assert!(
589                msg.contains("Unable to resolve spec"),
590                "expected 'Unable to resolve spec' in error, got: {msg}"
591            );
592        });
593    }
594
595    // ── clojure.spec.alpha (M2: explain + keys/merge) ────────────────────────
596
597    /// Asserts that a Clojure expression evaluates to exactly `true`/`false`.
598    #[track_caller]
599    fn assert_bool(src: &str, expected: bool, env: &mut Env) {
600        assert_eq!(
601            run(src, env).unwrap(),
602            Value::Bool(expected),
603            "expression: {src}"
604        );
605    }
606
607    #[test]
608    fn test_spec_keys_req_opt() {
609        run_with_big_stack(|| {
610            let (_, mut env) = make_env();
611            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
612            run("(s/def ::a int?) (s/def ::b string?)", &mut env).unwrap();
613            run("(s/def ::m (s/keys :req [::a] :opt [::b]))", &mut env).unwrap();
614            assert_bool("(s/valid? ::m {::a 1})", true, &mut env);
615            assert_bool("(s/valid? ::m {::a 1 ::b \"x\"})", true, &mut env);
616            assert_bool("(s/valid? ::m {::b \"x\"})", false, &mut env); // missing req
617            assert_bool("(s/valid? ::m {::a \"no\"})", false, &mut env); // bad req value
618            assert_bool("(s/valid? ::m {::a 1 ::b 2})", false, &mut env); // bad opt value
619            assert_bool("(s/valid? ::m 42)", false, &mut env); // not a map
620            // undeclared-but-registered key is still validated (upstream semantics)
621            assert_bool(
622                "(s/valid? (s/keys :req [::a]) {::a 1 ::b :not-a-string})",
623                false,
624                &mut env,
625            );
626            // connectives in :req
627            run("(s/def ::c boolean?)", &mut env).unwrap();
628            run(
629                "(s/def ::conn (s/keys :req [(or ::a (and ::b ::c))]))",
630                &mut env,
631            )
632            .unwrap();
633            assert_bool("(s/valid? ::conn {::a 1})", true, &mut env);
634            assert_bool("(s/valid? ::conn {::b \"x\" ::c true})", true, &mut env);
635            assert_bool("(s/valid? ::conn {::b \"x\"})", false, &mut env);
636            assert_bool("(s/valid? ::conn {})", false, &mut env);
637        });
638    }
639
640    #[test]
641    fn test_spec_keys_un_variants() {
642        run_with_big_stack(|| {
643            let (_, mut env) = make_env();
644            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
645            run("(s/def ::a int?) (s/def ::b string?)", &mut env).unwrap();
646            run(
647                "(s/def ::mu (s/keys :req-un [::a] :opt-un [::b]))",
648                &mut env,
649            )
650            .unwrap();
651            assert_bool("(s/valid? ::mu {:a 1})", true, &mut env);
652            assert_bool("(s/valid? ::mu {:a 1 :b \"x\"})", true, &mut env);
653            assert_bool("(s/valid? ::mu {:b \"x\"})", false, &mut env); // missing req-un
654            assert_bool("(s/valid? ::mu {:a \"no\"})", false, &mut env); // bad value
655            assert_bool("(s/valid? ::mu {:a 1 :b 2})", false, &mut env); // bad opt-un value
656            assert_bool("(= {:a 1} (s/conform ::mu {:a 1}))", true, &mut env);
657        });
658    }
659
660    #[test]
661    fn test_spec_keys_record() {
662        run_with_big_stack(|| {
663            let (_, mut env) = make_env();
664            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
665            run("(defrecord Point [x y])", &mut env).unwrap();
666            run("(s/def ::x number?) (s/def ::y number?)", &mut env).unwrap();
667            run("(s/def ::point (s/keys :req-un [::x ::y]))", &mut env).unwrap();
668            assert_bool("(s/valid? ::point (->Point 1 2))", true, &mut env);
669            assert_bool("(s/valid? ::point (->Point \"a\" 2))", false, &mut env);
670            // conform on a record returns a record (assoc preserves type tag)
671            assert_bool(
672                "(record? (s/conform ::point (->Point 1 2)))",
673                true,
674                &mut env,
675            );
676        });
677    }
678
679    #[test]
680    fn test_spec_explain_data_shape() {
681        run_with_big_stack(|| {
682            let (_, mut env) = make_env();
683            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
684            run("(s/def ::a int?) (s/def ::b string?)", &mut env).unwrap();
685            run("(s/def ::m (s/keys :req [::a] :opt [::b]))", &mut env).unwrap();
686
687            // Missing-req problem: (contains? % :user/a)-style pred, path/in [].
688            run("(def ed1 (s/explain-data ::m {::b \"x\"}))", &mut env).unwrap();
689            assert_bool(
690                "(pos? (count (:clojure.spec.alpha/problems ed1)))",
691                true,
692                &mut env,
693            );
694            let pred = run(
695                "(pr-str (:pred (first (:clojure.spec.alpha/problems ed1))))",
696                &mut env,
697            )
698            .unwrap();
699            assert_eq!(pred, Value::string("(contains? % :user/a)"));
700            assert_bool(
701                "(= [] (:path (first (:clojure.spec.alpha/problems ed1))))",
702                true,
703                &mut env,
704            );
705
706            // Bad-value problem: path/in extended by the key, via extended by
707            // both the named keys spec and the failing key's spec.
708            run("(def ed2 (s/explain-data ::m {::a \"no\"}))", &mut env).unwrap();
709            run(
710                "(def p2 (first (:clojure.spec.alpha/problems ed2)))",
711                &mut env,
712            )
713            .unwrap();
714            assert_bool("(= [::a] (:path p2))", true, &mut env);
715            assert_bool("(= [::a] (:in p2))", true, &mut env);
716            assert_bool("(= [::m ::a] (:via p2))", true, &mut env);
717            assert_bool("(= \"no\" (:val p2))", true, &mut env);
718
719            // Valid value → nil.
720            assert_bool("(nil? (s/explain-data ::m {::a 4}))", true, &mut env);
721
722            // Whole-map value and spec recorded on the explain-data map.
723            assert_bool(
724                "(= {::a \"no\"} (:clojure.spec.alpha/value ed2))",
725                true,
726                &mut env,
727            );
728        });
729    }
730
731    #[test]
732    fn test_spec_merge() {
733        run_with_big_stack(|| {
734            let (_, mut env) = make_env();
735            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
736            run("(s/def ::a int?) (s/def ::b string?)", &mut env).unwrap();
737            run("(s/def ::ma (s/keys :req [::a]))", &mut env).unwrap();
738            run("(s/def ::mb (s/keys :req [::b]))", &mut env).unwrap();
739            run("(s/def ::mab (s/merge ::ma ::mb))", &mut env).unwrap();
740            assert_bool("(s/valid? ::mab {::a 1 ::b \"x\"})", true, &mut env);
741            assert_bool("(s/valid? ::mab {::a 1})", false, &mut env);
742            assert_bool(
743                "(= {::a 1 ::b \"x\"} (s/conform ::mab {::a 1 ::b \"x\"}))",
744                true,
745                &mut env,
746            );
747            assert_bool(
748                "(pos? (count (:clojure.spec.alpha/problems
749                               (s/explain-data ::mab {::a 1}))))",
750                true,
751                &mut env,
752            );
753        });
754    }
755
756    #[test]
757    fn test_spec_explain_str_and_printer() {
758        run_with_big_stack(|| {
759            let (_, mut env) = make_env();
760            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
761            run("(s/def ::a int?)", &mut env).unwrap();
762            run("(s/def ::m (s/keys :req [::a]))", &mut env).unwrap();
763            let estr = match run("(s/explain-str ::m {::a \"no\"})", &mut env).unwrap() {
764                Value::Str(s) => s.get().clone(),
765                other => panic!("expected string from explain-str, got {other:?}"),
766            };
767            assert!(
768                estr.contains("failed") && estr.contains("int?"),
769                "unexpected explain-str output: {estr}"
770            );
771            let ok = run("(s/explain-str ::m {::a 1})", &mut env).unwrap();
772            assert_eq!(ok, Value::string("Success!\n"));
773        });
774    }
775
776    #[test]
777    fn test_spec_keys_unform_roundtrip() {
778        run_with_big_stack(|| {
779            let (_, mut env) = make_env();
780            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
781            run("(s/def ::id (s/or :i int? :s string?))", &mut env).unwrap();
782            run("(s/def ::rm (s/keys :req-un [::id]))", &mut env).unwrap();
783            // conform tags the or-valued key; unform untags it.
784            assert_bool("(= {:id [:i 5]} (s/conform ::rm {:id 5}))", true, &mut env);
785            assert_bool(
786                "(= {:id 5} (s/unform ::rm (s/conform ::rm {:id 5})))",
787                true,
788                &mut env,
789            );
790        });
791    }
792
793    // ── clojure.spec.alpha (M3: regex engine — cat/alt/*/+/?/&) ─────────────
794
795    #[test]
796    fn test_spec_regex_cat_and_nesting() {
797        run_with_big_stack(|| {
798            let (_, mut env) = make_env();
799            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
800            assert_bool(
801                "(= {:a 1 :b \"x\"} (s/conform (s/cat :a int? :b string?) [1 \"x\"]))",
802                true,
803                &mut env,
804            );
805            // Nested regex splices into the same flat sequence.
806            assert_bool(
807                "(= {:a 1 :b [\"x\" \"y\"]}
808                    (s/conform (s/cat :a int? :b (s/* string?)) [1 \"x\" \"y\"]))",
809                true,
810                &mut env,
811            );
812            // Wrong element type / too short / too long are all invalid.
813            assert_bool(
814                "(s/invalid? (s/conform (s/cat :a int? :b string?) [1 2]))",
815                true,
816                &mut env,
817            );
818            assert_bool(
819                "(s/invalid? (s/conform (s/cat :a int? :b string?) [1]))",
820                true,
821                &mut env,
822            );
823            assert_bool(
824                "(s/invalid? (s/conform (s/cat :a int?) [1 2]))",
825                true,
826                &mut env,
827            );
828            // Non-sequential input is invalid, not an error.
829            assert_bool("(s/invalid? (s/conform (s/cat :a int?) 5))", true, &mut env);
830        });
831    }
832
833    #[test]
834    fn test_spec_regex_alt_star_plus_maybe() {
835        run_with_big_stack(|| {
836            let (_, mut env) = make_env();
837            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
838            // alt tags its branch.
839            assert_bool(
840                "(= [:i 5] (s/conform (s/alt :i int? :s string?) [5]))",
841                true,
842                &mut env,
843            );
844            assert_bool(
845                "(s/invalid? (s/conform (s/alt :i int? :s string?) [:kw]))",
846                true,
847                &mut env,
848            );
849            // * matches empty and many; fails on a bad element.
850            assert_bool("(= [] (s/conform (s/* int?) []))", true, &mut env);
851            assert_bool("(s/valid? (s/* int?) [])", true, &mut env);
852            assert_bool("(= [1 2 3] (s/conform (s/* int?) [1 2 3]))", true, &mut env);
853            assert_bool(
854                "(s/invalid? (s/conform (s/* int?) [1 :a 3]))",
855                true,
856                &mut env,
857            );
858            // + requires at least one.
859            assert_bool("(= [1] (s/conform (s/+ int?) [1]))", true, &mut env);
860            assert_bool("(s/invalid? (s/conform (s/+ int?) []))", true, &mut env);
861            // ? is optional: value when present, nil when absent.
862            assert_bool("(= 5 (s/conform (s/? int?) [5]))", true, &mut env);
863            assert_bool("(nil? (s/conform (s/? int?) []))", true, &mut env);
864            assert_bool("(s/valid? (s/? int?) [])", true, &mut env);
865            assert_bool("(s/invalid? (s/conform (s/? int?) [1 2]))", true, &mut env);
866        });
867    }
868
869    #[test]
870    fn test_spec_regex_amp_and_regex_inside_and() {
871        run_with_big_stack(|| {
872            let (_, mut env) = make_env();
873            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
874            run("(def even-count? (fn [xs] (even? (count xs))))", &mut env).unwrap();
875            // & applies post-predicates to the conformed regex result.
876            assert_bool(
877                "(= [1 2] (s/conform (s/& (s/* int?) even-count?) [1 2]))",
878                true,
879                &mut env,
880            );
881            assert_bool(
882                "(s/invalid? (s/conform (s/& (s/* int?) even-count?) [1 2 3]))",
883                true,
884                &mut env,
885            );
886            // A regex op nested in s/and conforms the whole seq first, then
887            // threads the conformed value through the remaining preds.
888            run("(def all-even? (fn [xs] (every? even? xs)))", &mut env).unwrap();
889            assert_bool(
890                "(= [2 4] (s/conform (s/and (s/* int?) all-even?) [2 4]))",
891                true,
892                &mut env,
893            );
894            assert_bool(
895                "(s/valid? (s/and (s/* int?) all-even?) [1 2])",
896                false,
897                &mut env,
898            );
899        });
900    }
901
902    #[test]
903    fn test_spec_regex_keyword_ref_children() {
904        run_with_big_stack(|| {
905            let (_, mut env) = make_env();
906            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
907            // A keyword ref to a NON-regex spec consumes exactly one element.
908            run("(s/def ::a int?)", &mut env).unwrap();
909            assert_bool(
910                "(= {:x 1 :y \"x\"} (s/conform (s/cat :x ::a :y string?) [1 \"x\"]))",
911                true,
912                &mut env,
913            );
914            // A keyword ref to a REGEX spec splices (upstream reg-resolve!
915            // semantics).
916            run("(s/def ::r (s/* int?))", &mut env).unwrap();
917            assert_bool(
918                "(= {:nums [1 2] :tail \"x\"}
919                    (s/conform (s/cat :nums ::r :tail string?) [1 2 \"x\"]))",
920                true,
921                &mut env,
922            );
923            // s/spec wrapping forces a nested one-element boundary instead.
924            assert_bool(
925                "(= {:nums [1 2] :tail \"x\"}
926                    (s/conform (s/cat :nums (s/spec (s/* int?)) :tail string?)
927                               [[1 2] \"x\"]))",
928                true,
929                &mut env,
930            );
931            // Registered regex works at top level via the keyword.
932            assert_bool("(= [1 2 3] (s/conform ::r [1 2 3]))", true, &mut env);
933        });
934    }
935
936    #[test]
937    fn test_spec_regex_explain_reasons() {
938        run_with_big_stack(|| {
939            let (_, mut env) = make_env();
940            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
941            // Premature end of input → "Insufficient input" at the missing key.
942            run(
943                "(def p-insuff (first (:clojure.spec.alpha/problems
944                                       (s/explain-data (s/cat :a int? :b string?) [1]))))",
945                &mut env,
946            )
947            .unwrap();
948            assert_bool(
949                "(= \"Insufficient input\" (:reason p-insuff))",
950                true,
951                &mut env,
952            );
953            assert_bool("(= [:b] (:path p-insuff))", true, &mut env);
954            // Leftover input → "Extra input" with :in pointing at the index.
955            run(
956                "(def p-extra (first (:clojure.spec.alpha/problems
957                                      (s/explain-data (s/cat :a int?) [1 2]))))",
958                &mut env,
959            )
960            .unwrap();
961            assert_bool("(= \"Extra input\" (:reason p-extra))", true, &mut env);
962            assert_bool("(= [1] (:in p-extra))", true, &mut env);
963            // Element failure mid-sequence: path names the cat key, in the index.
964            run(
965                "(def p-elem (first (:clojure.spec.alpha/problems
966                                     (s/explain-data (s/cat :a int? :b string?) [1 :bad]))))",
967                &mut env,
968            )
969            .unwrap();
970            assert_bool("(= [:b] (:path p-elem))", true, &mut env);
971            assert_bool("(= [1] (:in p-elem))", true, &mut env);
972            assert_bool("(= :bad (:val p-elem))", true, &mut env);
973        });
974    }
975
976    #[test]
977    fn test_spec_regex_unform_roundtrips() {
978        run_with_big_stack(|| {
979            let (_, mut env) = make_env();
980            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
981            run("(def even-count? (fn [xs] (even? (count xs))))", &mut env).unwrap();
982            for (spec, input) in [
983                ("(s/cat :a int? :b string?)", "[1 \"x\"]"),
984                ("(s/cat :a int? :b (s/* string?))", "[1 \"x\" \"y\"]"),
985                ("(s/alt :i int? :s string?)", "[5]"),
986                ("(s/* int?)", "[1 2 3]"),
987                ("(s/* int?)", "[]"),
988                ("(s/+ int?)", "[1 2]"),
989                ("(s/? int?)", "[5]"),
990                ("(s/? int?)", "[]"),
991                ("(s/& (s/* int?) even-count?)", "[1 2]"),
992            ] {
993                assert_bool(
994                    &format!(
995                        "(let [re {spec}] (= {input} (vec (s/unform re (s/conform re {input})))))"
996                    ),
997                    true,
998                    &mut env,
999                );
1000            }
1001        });
1002    }
1003
1004    #[test]
1005    fn test_spec_plain_map_is_not_a_spec() {
1006        run_with_big_stack(|| {
1007            let (_, mut env) = make_env();
1008            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
1009            let err = run("(s/conform {:a 1} 5)", &mut env).unwrap_err();
1010            let msg = format!("{err}");
1011            assert!(
1012                msg.contains("not a valid spec"),
1013                "expected 'not a valid spec' in error, got: {msg}"
1014            );
1015        });
1016    }
1017
1018    // ── clojure.spec.alpha (M4: collections + leaf specs) ────────────────────
1019
1020    #[test]
1021    fn test_spec_coll_of_options() {
1022        run_with_big_stack(|| {
1023            let (_, mut env) = make_env();
1024            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
1025            // happy / sad
1026            assert_bool(
1027                "(= [1 2 3] (s/conform (s/coll-of int?) [1 2 3]))",
1028                true,
1029                &mut env,
1030            );
1031            assert_bool(
1032                "(s/invalid? (s/conform (s/coll-of int?) [1 \"x\"]))",
1033                true,
1034                &mut env,
1035            );
1036            // :kind
1037            assert_bool(
1038                "(s/invalid? (s/conform (s/coll-of int? :kind vector?) '(1 2 3)))",
1039                true,
1040                &mut env,
1041            );
1042            // :min-count / :max-count
1043            assert_bool(
1044                "(s/invalid? (s/conform (s/coll-of int? :min-count 3) [1 2]))",
1045                true,
1046                &mut env,
1047            );
1048            assert_bool(
1049                "(s/invalid? (s/conform (s/coll-of int? :max-count 2) [1 2 3]))",
1050                true,
1051                &mut env,
1052            );
1053            // :distinct
1054            assert_bool(
1055                "(= [1 2 3] (s/conform (s/coll-of int? :distinct true) [1 2 3]))",
1056                true,
1057                &mut env,
1058            );
1059            assert_bool(
1060                "(s/invalid? (s/conform (s/coll-of int? :distinct true) [1 1 2]))",
1061                true,
1062                &mut env,
1063            );
1064            // :into
1065            assert_bool(
1066                "(= #{1 2 3} (s/conform (s/coll-of int? :into #{}) [1 2 3]))",
1067                true,
1068                &mut env,
1069            );
1070            // default (no :into) preserves order for list input; explicit
1071            // :into '() conjes raw (reverses) — see EverySpec doc comment.
1072            assert_bool(
1073                "(= '(1 2 3) (s/conform (s/coll-of int?) '(1 2 3)))",
1074                true,
1075                &mut env,
1076            );
1077            assert_bool(
1078                "(= '(3 2 1) (s/conform (s/coll-of int? :into '()) '(1 2 3)))",
1079                true,
1080                &mut env,
1081            );
1082        });
1083    }
1084
1085    #[test]
1086    fn test_spec_map_of_key_handling() {
1087        run_with_big_stack(|| {
1088            let (_, mut env) = make_env();
1089            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
1090            // default: keys must be valid but ORIGINAL keys are kept
1091            assert_bool(
1092                "(= {:a 1 :b 2} (s/conform (s/map-of keyword? int?) {:a 1 :b 2}))",
1093                true,
1094                &mut env,
1095            );
1096            assert_bool(
1097                "(s/invalid? (s/conform (s/map-of keyword? int?) {:a \"x\"}))",
1098                true,
1099                &mut env,
1100            );
1101            assert_bool(
1102                "(s/invalid? (s/conform (s/map-of keyword? int?) {\"not-kw\" 1}))",
1103                true,
1104                &mut env,
1105            );
1106            // :conform-keys true swaps in the conformed key
1107            assert_bool(
1108                r#"(= {"a" 1} (s/conform (s/map-of (s/conformer name) int? :conform-keys true) {:a 1}))"#,
1109                true,
1110                &mut env,
1111            );
1112        });
1113    }
1114
1115    #[test]
1116    fn test_spec_every_vs_coll_of_conform_difference() {
1117        run_with_big_stack(|| {
1118            let (_, mut env) = make_env();
1119            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
1120            run("(def elem (s/or :i int? :s string?))", &mut env).unwrap();
1121            // coll-of conforms every element (tagged vectors)
1122            assert_bool(
1123                "(= [[:i 1] [:s \"x\"]] (s/conform (s/coll-of elem) [1 \"x\"]))",
1124                true,
1125                &mut env,
1126            );
1127            // every only validates — returns x unchanged
1128            assert_bool(
1129                "(= [1 \"x\"] (s/conform (s/every elem) [1 \"x\"]))",
1130                true,
1131                &mut env,
1132            );
1133            assert_bool(
1134                "(s/invalid? (s/conform (s/every elem) [1 :bad]))",
1135                true,
1136                &mut env,
1137            );
1138            // every-kv validates both k/v but never rebuilds
1139            assert_bool(
1140                "(= {:a 1} (s/conform (s/every-kv keyword? int?) {:a 1}))",
1141                true,
1142                &mut env,
1143            );
1144            assert_bool(
1145                "(s/invalid? (s/conform (s/every-kv keyword? int?) {:a \"x\"}))",
1146                true,
1147                &mut env,
1148            );
1149        });
1150    }
1151
1152    #[test]
1153    fn test_spec_tuple_conform_and_explain() {
1154        run_with_big_stack(|| {
1155            let (_, mut env) = make_env();
1156            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
1157            assert_bool(
1158                "(= [1 \"x\"] (s/conform (s/tuple int? string?) [1 \"x\"]))",
1159                true,
1160                &mut env,
1161            );
1162            assert_bool(
1163                "(s/invalid? (s/conform (s/tuple int? string?) [1]))",
1164                true,
1165                &mut env,
1166            );
1167            assert_bool(
1168                "(s/invalid? (s/conform (s/tuple int? string?) 5))",
1169                true,
1170                &mut env,
1171            );
1172            run(
1173                "(def tp (first (:clojure.spec.alpha/problems (s/explain-data (s/tuple int? string?) [1 :bad]))))",
1174                &mut env,
1175            )
1176            .unwrap();
1177            assert_bool("(= [1] (:path tp))", true, &mut env);
1178            assert_bool("(= [1] (:in tp))", true, &mut env);
1179            assert_bool("(= :bad (:val tp))", true, &mut env);
1180            // round-trip unform
1181            assert_bool(
1182                "(= [1 \"x\"] (s/unform (s/tuple int? string?) (s/conform (s/tuple int? string?) [1 \"x\"])))",
1183                true,
1184                &mut env,
1185            );
1186        });
1187    }
1188
1189    #[test]
1190    fn test_spec_nilable_both_branches_and_explain_shape() {
1191        run_with_big_stack(|| {
1192            let (_, mut env) = make_env();
1193            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
1194            assert_bool("(= nil (s/conform (s/nilable int?) nil))", true, &mut env);
1195            assert_bool("(= 5 (s/conform (s/nilable int?) 5))", true, &mut env);
1196            assert_bool(
1197                "(s/invalid? (s/conform (s/nilable int?) \"x\"))",
1198                true,
1199                &mut env,
1200            );
1201            run(
1202                "(def np (:clojure.spec.alpha/problems (s/explain-data (s/nilable int?) \"x\")))",
1203                &mut env,
1204            )
1205            .unwrap();
1206            assert_bool("(= 2 (count np))", true, &mut env);
1207            assert_bool(
1208                "(some (fn [p] (= [:clojure.spec.alpha/nil] (:path p))) np)",
1209                true,
1210                &mut env,
1211            );
1212            assert_bool(
1213                "(some (fn [p] (= [:clojure.spec.alpha/pred] (:path p))) np)",
1214                true,
1215                &mut env,
1216            );
1217        });
1218    }
1219
1220    #[test]
1221    fn test_spec_multi_spec_conform_and_no_method() {
1222        run_with_big_stack(|| {
1223            let (_, mut env) = make_env();
1224            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
1225            run("(defmulti event-type :type)", &mut env).unwrap();
1226            run("(s/def :evt/type keyword?)", &mut env).unwrap();
1227            run("(s/def :evt/a (s/keys :req-un [:evt/type]))", &mut env).unwrap();
1228            run("(defmethod event-type :a [_] :evt/a)", &mut env).unwrap();
1229            run(
1230                "(s/def :evt/event (s/multi-spec event-type :type))",
1231                &mut env,
1232            )
1233            .unwrap();
1234            assert_bool(
1235                "(= {:type :a} (s/conform :evt/event {:type :a}))",
1236                true,
1237                &mut env,
1238            );
1239            assert_bool(
1240                "(s/invalid? (s/conform :evt/event {:type :unknown}))",
1241                true,
1242                &mut env,
1243            );
1244            run(
1245                "(def mp (first (:clojure.spec.alpha/problems (s/explain-data :evt/event {:type :unknown}))))",
1246                &mut env,
1247            )
1248            .unwrap();
1249            assert_bool("(= \"no method\" (:reason mp))", true, &mut env);
1250        });
1251    }
1252
1253    #[test]
1254    fn test_spec_conformer_transforms_and_threads_through_and() {
1255        run_with_big_stack(|| {
1256            let (_, mut env) = make_env();
1257            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
1258            assert_bool(
1259                "(= 10 (s/conform (s/conformer #(* 2 %)) 5))",
1260                true,
1261                &mut env,
1262            );
1263            // threads the transformed value through the rest of s/and
1264            assert_bool(
1265                "(= 10 (s/conform (s/and int? (s/conformer #(* 2 %))) 5))",
1266                true,
1267                &mut env,
1268            );
1269            assert_bool(
1270                "(= 5 (s/unform (s/conformer #(* 2 %) #(/ % 2)) 10))",
1271                true,
1272                &mut env,
1273            );
1274            // no unf given -> unform is identity
1275            assert_bool(
1276                "(= 10 (s/unform (s/conformer #(* 2 %)) 10))",
1277                true,
1278                &mut env,
1279            );
1280        });
1281    }
1282
1283    #[test]
1284    fn test_spec_int_in_and_double_in_bounds() {
1285        run_with_big_stack(|| {
1286            let (_, mut env) = make_env();
1287            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
1288            assert_bool("(s/valid? (s/int-in 0 10) 5)", true, &mut env);
1289            assert_bool("(s/valid? (s/int-in 0 10) 10)", false, &mut env); // end exclusive
1290            assert_bool("(s/valid? (s/int-in 0 10) 5.0)", false, &mut env); // not an int?
1291            assert_bool(
1292                "(s/valid? (s/double-in :min 0.0 :max 10.0) 5.0)",
1293                true,
1294                &mut env,
1295            );
1296            assert_bool(
1297                "(s/valid? (s/double-in :min 0.0 :max 10.0) 20.0)",
1298                false,
1299                &mut env,
1300            );
1301            // NaN produced via arithmetic (NOT the ##NaN reader literal — that
1302            // literal hangs this runtime indefinitely on eval, a pre-existing
1303            // bug unrelated to spec.alpha; see M4 report).
1304            run("(def nan (/ 0.0 0.0))", &mut env).unwrap();
1305            run("(def pos-inf (/ 1.0 0.0))", &mut env).unwrap();
1306            assert_bool("(s/valid? (s/double-in :NaN? false) nan)", false, &mut env);
1307            assert_bool("(s/valid? (s/double-in) nan)", true, &mut env);
1308            assert_bool(
1309                "(s/valid? (s/double-in :infinite? false) pos-inf)",
1310                false,
1311                &mut env,
1312            );
1313            assert_bool("(s/valid? (s/double-in) pos-inf)", true, &mut env);
1314            // nonconforming: validates but returns x unconformed
1315            assert_bool(
1316                "(= [1 \"x\"] (s/conform (s/nonconforming (s/cat :a int? :b string?)) [1 \"x\"]))",
1317                true,
1318                &mut env,
1319            );
1320            // inst-in: not implemented, throws clearly
1321            let err = run("(s/inst-in 0 1)", &mut env).unwrap_err();
1322            let msg = format!("{err}");
1323            assert!(
1324                msg.contains("not implemented"),
1325                "expected 'not implemented' in error, got: {msg}"
1326            );
1327        });
1328    }
1329
1330    // ── clojure.spec.alpha / .test.alpha / .gen.alpha (M5: fdef/instrument) ──
1331
1332    #[test]
1333    fn test_spec_fdef_registers_qualified_symbol_and_get_spec() {
1334        run_with_big_stack(|| {
1335            let (_, mut env) = make_env();
1336            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
1337            run("(defn my-add [a b] (+ a b))", &mut env).unwrap();
1338            assert_bool(
1339                "(= 'user/my-add (s/fdef my-add :args (s/cat :a int? :b int?) :ret int?))",
1340                true,
1341                &mut env,
1342            );
1343            assert_bool("(s/fspec? (s/get-spec 'user/my-add))", true, &mut env);
1344            assert_bool("(some? (:args (s/get-spec 'user/my-add)))", true, &mut env);
1345            // syntax-quote auto-qualification against the current ns resolves
1346            // to the same registry entry as the explicit qualified symbol.
1347            assert_bool(
1348                "(= (s/get-spec 'user/my-add) (s/get-spec `my-add))",
1349                true,
1350                &mut env,
1351            );
1352        });
1353    }
1354
1355    #[test]
1356    fn test_spec_instrument_valid_and_invalid_calls() {
1357        run_with_big_stack(|| {
1358            let (_, mut env) = make_env();
1359            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
1360            run("(require '[clojure.spec.test.alpha :as stest])", &mut env).unwrap();
1361            run("(defn my-add [a b] (+ a b))", &mut env).unwrap();
1362            run(
1363                "(s/fdef my-add :args (s/cat :a int? :b int?) :ret int?)",
1364                &mut env,
1365            )
1366            .unwrap();
1367            run("(stest/instrument 'user/my-add)", &mut env).unwrap();
1368            // valid call still returns the normal value through the wrapper.
1369            assert_eq!(run("(my-add 2 3)", &mut env).unwrap(), Value::Long(5));
1370            // invalid call throws with the expected message and ex-data.
1371            run(
1372                r#"(def caught
1373                     (try (my-add 2 "x") :did-not-throw
1374                          (catch Exception e {:msg (ex-message e) :data (ex-data e)})))"#,
1375                &mut env,
1376            )
1377            .unwrap();
1378            assert_bool(
1379                r#"(= "Call to user/my-add did not conform to spec." (:msg caught))"#,
1380                true,
1381                &mut env,
1382            );
1383            assert_bool(
1384                "(= :instrument (:clojure.spec.alpha/failure (:data caught)))",
1385                true,
1386                &mut env,
1387            );
1388            assert_bool(
1389                "(contains? (:data caught) :clojure.spec.test.alpha/caller)",
1390                true,
1391                &mut env,
1392            );
1393        });
1394    }
1395
1396    #[test]
1397    fn test_spec_unstrument_restores_raw_fn() {
1398        run_with_big_stack(|| {
1399            let (_, mut env) = make_env();
1400            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
1401            run("(require '[clojure.spec.test.alpha :as stest])", &mut env).unwrap();
1402            run("(defn my-pair [a b] [a b])", &mut env).unwrap();
1403            run("(s/fdef my-pair :args (s/cat :a int? :b int?))", &mut env).unwrap();
1404            run("(stest/instrument 'user/my-pair)", &mut env).unwrap();
1405            assert!(run("(my-pair 1 \"x\")", &mut env).is_err());
1406            run("(stest/unstrument 'user/my-pair)", &mut env).unwrap();
1407            // no longer wrapped -- the raw fn has no type constraints, so an
1408            // "invalid by spec" call now succeeds instead of throwing.
1409            assert_bool("(= [1 \"x\"] (my-pair 1 \"x\"))", true, &mut env);
1410        });
1411    }
1412
1413    #[test]
1414    fn test_spec_instrument_affects_call_sites_evaluated_before_instrument() {
1415        run_with_big_stack(|| {
1416            let (_, mut env) = make_env();
1417            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
1418            run("(require '[clojure.spec.test.alpha :as stest])", &mut env).unwrap();
1419            run("(defn my-mul [a b] (* a b))", &mut env).unwrap();
1420            run("(s/fdef my-mul :args (s/cat :a int? :b int?))", &mut env).unwrap();
1421            // Warm up this call site (and any inline-cached/lowered code for
1422            // it) BEFORE instrumenting.
1423            assert_eq!(run("(my-mul 2 3)", &mut env).unwrap(), Value::Long(6));
1424            run("(stest/instrument 'user/my-mul)", &mut env).unwrap();
1425            // A stale inline cache pointing at the pre-instrument var root
1426            // would silently skip the spec check here.
1427            assert!(run("(my-mul 2 \"x\")", &mut env).is_err());
1428        });
1429    }
1430
1431    #[test]
1432    fn test_spec_instrumentable_syms_and_idempotent_double_instrument() {
1433        run_with_big_stack(|| {
1434            let (_, mut env) = make_env();
1435            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
1436            run("(require '[clojure.spec.test.alpha :as stest])", &mut env).unwrap();
1437            run("(defn my-pair2 [a b] [a b])", &mut env).unwrap();
1438            run("(s/fdef my-pair2 :args (s/cat :a int? :b int?))", &mut env).unwrap();
1439            assert_bool(
1440                "(boolean (some #{'user/my-pair2} (stest/instrumentable-syms)))",
1441                true,
1442                &mut env,
1443            );
1444            run("(stest/instrument 'user/my-pair2)", &mut env).unwrap();
1445            run("(stest/instrument 'user/my-pair2)", &mut env).unwrap(); // idempotent
1446            assert!(run("(my-pair2 1 \"x\")", &mut env).is_err());
1447            // A SINGLE unstrument must fully restore -- if double-instrument
1448            // had double-wrapped, one layer would still be active and the
1449            // call below would still throw.
1450            run("(stest/unstrument 'user/my-pair2)", &mut env).unwrap();
1451            assert_bool("(= [1 \"x\"] (my-pair2 1 \"x\"))", true, &mut env);
1452        });
1453    }
1454
1455    #[test]
1456    fn test_spec_with_instrument_disabled_bypasses_checks() {
1457        run_with_big_stack(|| {
1458            let (_, mut env) = make_env();
1459            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
1460            run("(require '[clojure.spec.test.alpha :as stest])", &mut env).unwrap();
1461            run("(defn my-pair3 [a b] [a b])", &mut env).unwrap();
1462            run("(s/fdef my-pair3 :args (s/cat :a int? :b int?))", &mut env).unwrap();
1463            run("(stest/instrument 'user/my-pair3)", &mut env).unwrap();
1464            assert!(run("(my-pair3 1 \"x\")", &mut env).is_err());
1465            assert_bool(
1466                "(stest/with-instrument-disabled (= [1 \"x\"] (my-pair3 1 \"x\")))",
1467                true,
1468                &mut env,
1469            );
1470            // instrumentation resumes outside the dynamic extent.
1471            assert!(run("(my-pair3 1 \"x\")", &mut env).is_err());
1472        });
1473    }
1474
1475    #[test]
1476    fn test_spec_assert_happy_sad_and_check_asserts_toggle() {
1477        run_with_big_stack(|| {
1478            let (_, mut env) = make_env();
1479            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
1480            assert_eq!(run("(s/assert even? 4)", &mut env).unwrap(), Value::Long(4));
1481            assert!(run("(s/assert even? 5)", &mut env).is_err());
1482            run("(s/check-asserts false)", &mut env).unwrap();
1483            // a freshly (re-)macroexpanded s/assert form reads
1484            // *compile-asserts* at macroexpansion time and passes an invalid
1485            // value through unchecked.
1486            assert_eq!(run("(s/assert even? 5)", &mut env).unwrap(), Value::Long(5));
1487            run("(s/check-asserts true)", &mut env).unwrap();
1488            assert!(run("(s/assert even? 5)", &mut env).is_err());
1489        });
1490    }
1491
1492    #[test]
1493    fn test_spec_gen_and_check_throw_not_implemented_and_gen_ns_loads() {
1494        run_with_big_stack(|| {
1495            let (_, mut env) = make_env();
1496            run("(require '[clojure.spec.alpha :as s])", &mut env).unwrap();
1497            run("(require '[clojure.spec.test.alpha :as stest])", &mut env).unwrap();
1498            run("(require '[clojure.spec.gen.alpha :as gen])", &mut env).unwrap();
1499            for src in [
1500                "(s/gen even?)",
1501                "(s/exercise even?)",
1502                "(s/exercise-fn 'user/foo)",
1503                "(stest/check)",
1504                "(stest/check 'user/foo)",
1505                "(stest/check-fn (fn [x] x) even?)",
1506                "(gen/generate :whatever)",
1507                "(gen/sample :whatever)",
1508                "(gen/elements [1 2 3])",
1509            ] {
1510                assert!(run(src, &mut env).is_err(), "expected {src} to throw");
1511            }
1512        });
1513    }
1514
1515    // ── clojure.java.shell ────────────────────────────────────────────────────
1516
1517    #[cfg(not(target_arch = "wasm32"))]
1518    fn shell_env() -> Env {
1519        let (_, mut env) = make_env();
1520        run(
1521            "(require '[clojure.java.shell :refer [sh with-sh-dir with-sh-env]])",
1522            &mut env,
1523        )
1524        .unwrap();
1525        // A string of at least n chars, doubled rather than built from a lazy
1526        // `repeat`, which costs ~0.2 ms per element in the interpreter.
1527        run(
1528            "(defn big [s n] (loop [s s] (if (>= (count s) n) s (recur (str s s)))))",
1529            &mut env,
1530        )
1531        .unwrap();
1532        env
1533    }
1534
1535    #[cfg(not(target_arch = "wasm32"))]
1536    fn truthy(src: &str, env: &mut Env) {
1537        assert_eq!(run(src, env).unwrap(), Value::Bool(true), "{src}");
1538    }
1539
1540    #[test]
1541    #[cfg(unix)]
1542    fn test_sh_exit_is_data() {
1543        let mut env = shell_env();
1544        truthy(
1545            "(= {:exit 0 :out \"hi\" :err \"\"} (sh \"printf\" \"hi\"))",
1546            &mut env,
1547        );
1548        truthy(
1549            "(= {:exit 3 :out \"\" :err \"e\"} (sh \"sh\" \"-c\" \"printf e >&2; exit 3\"))",
1550            &mut env,
1551        );
1552        truthy(
1553            "(= 137 (:exit (sh \"sh\" \"-c\" \"kill -9 $$\")))",
1554            &mut env,
1555        );
1556    }
1557
1558    #[test]
1559    #[cfg(unix)]
1560    fn test_sh_in_dir_env() {
1561        let mut env = shell_env();
1562        truthy("(= \"piped\" (:out (sh \"cat\" :in \"piped\")))", &mut env);
1563        truthy(
1564            "(= 262144 (count (:out (sh \"cat\" :in (big \"x\" 262144)))))",
1565            &mut env,
1566        );
1567        truthy("(= \"/\\n\" (:out (sh \"pwd\" :dir \"/\")))", &mut env);
1568        truthy(
1569            "(= \"/\\n\" (:out (with-sh-dir \"/\" (sh \"pwd\"))))",
1570            &mut env,
1571        );
1572        truthy(
1573            "(= \"FOO=bar\\n\" (:out (sh \"/usr/bin/env\" :env {\"FOO\" \"bar\"})))",
1574            &mut env,
1575        );
1576        truthy(
1577            "(= \"A=1=2\\n\" (:out (with-sh-env [\"A=1=2\"] (sh \"/usr/bin/env\"))))",
1578            &mut env,
1579        );
1580        // The program is found on the parent's PATH although :env replaces it.
1581        truthy(
1582            "(= \"x\" (:out (sh \"printf\" \"x\" :env {\"X\" \"1\"})))",
1583            &mut env,
1584        );
1585        // A child that exits without reading a large :in does not kill us.
1586        truthy(
1587            "(= 0 (:exit (sh \"true\" :in (big \"y\" 1048576))))",
1588            &mut env,
1589        );
1590    }
1591
1592    #[test]
1593    #[cfg(unix)]
1594    fn test_sh_out_bytes() {
1595        let mut env = shell_env();
1596        truthy(
1597            "(= [97 98] (vec (:out (sh \"printf\" \"ab\" :out-enc :bytes))))",
1598            &mut env,
1599        );
1600    }
1601
1602    #[test]
1603    #[cfg(unix)]
1604    fn test_sh_refusals() {
1605        let mut env = shell_env();
1606        for src in [
1607            "(sh \"cljrs-no-such-program-x\")",
1608            "(sh \"pwd\" :dir \"/no/such/dir\")",
1609            "(sh \"printf\" \"x\" :env {\"A=B\" \"1\"})",
1610            "(sh \"printf\" \"x\" :env {\"A\" \"\\u0000\"})",
1611            "(sh \"printf\" \"x\" :out-enc \"latin1\")",
1612            "(sh :in \"x\")",
1613            "(clojure.rust.process/run [])",
1614        ] {
1615            assert!(run(src, &mut env).is_err(), "expected {src} to throw");
1616        }
1617    }
1618
1619    /// `s` as a Clojure string literal: printable ASCII kept, `"` and `\`
1620    /// escaped, everything else (NUL included) spelled `\uXXXX`.
1621    #[cfg(unix)]
1622    fn clj_string_lit(s: &str) -> String {
1623        let mut out = String::from("\"");
1624        for c in s.chars() {
1625            match c {
1626                '"' => out.push_str("\\\""),
1627                '\\' => out.push_str("\\\\"),
1628                ' '..='~' => out.push(c),
1629                _ => out.push_str(&format!("\\u{:04x}", c as u32)),
1630            }
1631        }
1632        out.push('"');
1633        out
1634    }
1635
1636    /// A string of Basic Multilingual Plane characters, NUL excluded: the
1637    /// `\uXXXX` escape spells exactly these.
1638    #[cfg(unix)]
1639    fn bmp_string(max: usize) -> impl proptest::strategy::Strategy<Value = String> {
1640        use proptest::prelude::*;
1641        prop::collection::vec(prop::char::range('\u{1}', '\u{ffff}'), 0..max)
1642            .prop_map(|cs| cs.into_iter().collect())
1643    }
1644
1645    /// Law 1: an :env value holding a NUL anywhere is refused with an error,
1646    /// never a panic out of `Command::env`. Law 2: every NUL-free value
1647    /// reaches the child byte for byte. Law 2 is what keeps law 1 honest: a
1648    /// fix that refused every value would pass law 1 alone.
1649    #[test]
1650    #[cfg(unix)]
1651    fn test_env_value_nul_laws() {
1652        use proptest::test_runner::{Config, TestRunner};
1653        use std::cell::RefCell;
1654        let env = RefCell::new(shell_env());
1655        let mut runner = TestRunner::new(Config::with_cases(48));
1656
1657        runner
1658            .run(&(bmp_string(8), bmp_string(8)), |(pre, post)| {
1659                let v = format!("{pre}\0{post}");
1660                let src = format!(
1661                    "(clojure.rust.process/run [\"printf\" \"x\"] {{:env {{\"A\" {}}}}})",
1662                    clj_string_lit(&v)
1663                );
1664                proptest::prop_assert!(run(&src, &mut env.borrow_mut()).is_err(), "{src}");
1665                Ok(())
1666            })
1667            .unwrap();
1668
1669        runner
1670            .run(&bmp_string(12), |v| {
1671                let src = format!(
1672                    "(let [v {}] (= (str \"A=\" v \"\\n\") \
1673                     (:out (clojure.rust.process/run [\"/usr/bin/env\"] {{:env {{\"A\" v}}}}))))",
1674                    clj_string_lit(&v)
1675                );
1676                let got = run(&src, &mut env.borrow_mut());
1677                proptest::prop_assert_eq!(got.ok(), Some(Value::Bool(true)), "{}", src);
1678                Ok(())
1679            })
1680            .unwrap();
1681    }
1682
1683    /// Golden record of the process module's :env answers, refusals and
1684    /// successes alike, byte for byte, so an unintended change shows up as a
1685    /// diff in review. Regenerate after an intentional change:
1686    ///
1687    /// ```sh
1688    /// UPDATE_GOLDEN=1 cargo test -p cljrs-stdlib test_process_env_golden
1689    /// ```
1690    #[test]
1691    #[cfg(unix)]
1692    fn test_process_env_golden() {
1693        const GOLDEN: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/golden/process_env.txt");
1694        const ROWS: &[&str] = &[
1695            "(:out (clojure.rust.process/run [\"printf\" \"x\"] {:env {\"A\" \"ok\"}}))",
1696            "(:out (clojure.rust.process/run [\"/usr/bin/env\"] {:env {\"A\" \"a b=c\"}}))",
1697            "(:out (clojure.rust.process/run [\"/usr/bin/env\"] {:env {:A \"kw\"}}))",
1698            "(clojure.rust.process/run [\"printf\" \"x\"] {:env {\"A\" \"\\u0000\"}})",
1699            "(clojure.rust.process/run [\"printf\" \"x\"] {:env {\"A\" \"a\\u0000b\"}})",
1700            "(clojure.rust.process/run [\"printf\" \"x\"] {:env {\"A=B\" \"1\"}})",
1701            "(clojure.rust.process/run [\"printf\" \"x\"] {:env {\"A\" 1}})",
1702            "(clojure.rust.process/run [\"printf\" \"x\"] {:env [\"A\" \"1\"]})",
1703            "(clojure.rust.process/run [])",
1704        ];
1705        let mut env = shell_env();
1706        let mut actual = String::new();
1707        for src in ROWS {
1708            let outcome = match run(src, &mut env) {
1709                Ok(v) => format!("{v:?}"),
1710                Err(e) => {
1711                    let text = format!("{e:?}").replace('\n', " ");
1712                    let msg = text
1713                        .split("message: \"")
1714                        .nth(1)
1715                        .and_then(|s| s.split('"').next())
1716                        .or_else(|| text.split('"').nth(1))
1717                        .map(str::to_string)
1718                        .unwrap_or(text);
1719                    format!("ERROR {msg}")
1720                }
1721            };
1722            actual.push_str(&format!("{src}\n  => {outcome}\n"));
1723        }
1724        if std::env::var_os("UPDATE_GOLDEN").is_some() {
1725            std::fs::create_dir_all(std::path::Path::new(GOLDEN).parent().unwrap()).unwrap();
1726            std::fs::write(GOLDEN, &actual).unwrap();
1727            return;
1728        }
1729        let expected = std::fs::read_to_string(GOLDEN)
1730            .unwrap_or_else(|e| panic!("{GOLDEN}: {e}; run with UPDATE_GOLDEN=1 to create it"));
1731        assert_eq!(
1732            actual, expected,
1733            "process :env answers changed; see {GOLDEN}"
1734        );
1735    }
1736
1737    #[test]
1738    #[cfg(not(target_arch = "wasm32"))]
1739    fn test_process_run_denied_under_transaction_policy() {
1740        use cljrs_runtime::env::policy::{TransactionPolicyGuard, check_native};
1741        assert!(check_native(process::RUN_NAME).is_ok());
1742        let _guard = TransactionPolicyGuard::install();
1743        assert!(check_native(process::RUN_NAME).is_err());
1744    }
1745}