Skip to main content

cljrs_stdlib/
lib.rs

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