1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
//! Regression test: a `require` spec may carry a reader conditional in its
//! namespace slot.
//!
//! `[#?(:clj clojure.core :cljs cljs.core) :as core]` is the idiom every
//! clj+cljs `.cljc` library uses to alias the host core. `clojure.test.check`
//! opens with it. The option loop already resolved conditionals in the trailing
//! `:as` / `:refer` positions, but the first element did not get the same
//! treatment, so the spec read as `[:as core]` and was rejected with "require
//! spec: first element must be a symbol".
//!
//! Every case here goes through an `ns` form on purpose. That is the path that
//! parses raw `Form`s, so the conditional is still an unresolved `ReaderCond`
//! when the spec is read. A quoted `(require '[#?(…) :as core])` does NOT
//! exercise it: evaluating the quote resolves the conditional first, so the
//! spec parser only ever sees the selected branch.
use std::sync::Arc;
use cljrs_reader::Parser;
use cljrs_runtime::env::env::{Env, GlobalEnv};
use cljrs_value::Value;
fn make_env() -> (Arc<GlobalEnv>, Env) {
let globals = cljrs_runtime::Runtime::builder()
.execution_mode(cljrs_runtime::ExecutionMode::TreeWalk)
.eager_clojure_test(true)
.build()
.expect("runtime")
.into_globals();
let env = Env::new(globals.clone(), "user");
(globals, env)
}
/// Assert `result` is `42`, the marker every positive case evaluates to
/// through the alias, proving the aliased namespace actually resolved.
///
/// The aliased namespace is `clojure.core`, which is exactly the shape
/// test.check uses (`[#?(:clj clojure.core :cljs cljs.core) :as core]`) and
/// the only one guaranteed present in a bare interpreter env: this harness
/// has no source path, so nothing loadable from disk can be relied on.
fn assert_marker(result: Value) {
match result {
Value::Long(n) => assert_eq!(n, 42),
other => panic!("expected 42, got {other:?}"),
}
}
fn eval_fresh(src: &str) -> Result<Value, String> {
let (_, mut env) = make_env();
let mut parser = Parser::new(src.to_string(), "<test>".to_string());
let forms = parser.parse_all().map_err(|e| format!("{e:?}"))?;
let mut result = Value::Nil;
for form in forms {
result =
cljrs_runtime::interp::eval::eval(&form, &mut env).map_err(|e| format!("{e:?}"))?;
}
Ok(result)
}
#[test]
fn namespace_slot_resolves_a_reader_conditional() {
let result = eval_fresh(
"(ns t.a (:require [#?(:rust clojure.core :default clojure.core) :as core])) \
(core/inc 41)",
)
.expect("spec with a conditional namespace should load");
assert_marker(result);
}
#[test]
fn default_branch_is_taken_when_no_platform_key_matches() {
let result = eval_fresh(
"(ns t.b (:require [#?(:clj clojure.set :cljs cljs.core :default clojure.core) :as core])) \
(core/inc 41)",
)
.expect("the :default branch should be selected on :rust");
assert_marker(result);
}
/// The trailing options already resolved conditionals; that must keep working
/// now that the head is resolved too.
#[test]
fn option_slot_conditionals_still_resolve() {
let result = eval_fresh(
"(ns t.c (:require [clojure.core #?(:cljs :refer-macros :default :as) core])) \
(core/inc 41)",
)
.expect("a conditional in the option position is the case that already worked");
assert_marker(result);
}
/// A plain spec must be untouched by the new resolution step.
#[test]
fn plain_namespace_slot_is_unaffected() {
let result = eval_fresh("(ns t.d (:require [clojure.core :as core])) (core/inc 41)")
.expect("plain spec should be unaffected");
assert_marker(result);
}
/// When no branch matches there is no namespace to require, so the spec is
/// refused by name rather than reported as a malformed first element. The
/// message must name the real cause: `#?(:clj … :cljs …)` with no `:default`
/// is unloadable on this runtime, and the caller needs to know it is the
/// missing branch and not their spelling. `clojure.test.check` is exactly
/// this shape.
#[test]
fn no_matching_branch_is_reported_as_such() {
let err = eval_fresh("(ns t.e (:require [#?(:clj clojure.core :cljs cljs.core) :as core]))")
.expect_err("no branch matches :rust");
assert!(
err.contains("no reader-conditional branch matched"),
"message should name the real cause, got: {err}"
);
}