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