Skip to main content

cljrs_stdlib/
lib.rs

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