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