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