Skip to main content

cljrs_stdlib/
lib.rs

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