use std::sync::Arc;
use cljrs_reader::Parser;
use cljrs_runtime::env::env::{Env, GlobalEnv};
use cljrs_value::Value;
use proptest::prelude::*;
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)
}
fn eval_in(env: &mut Env, src: &str) -> Result<String, String> {
let mut parser = Parser::new(src.to_string(), "<prop>".to_string());
let forms = parser.parse_all().map_err(|e| format!("parse: {e:?}"))?;
let mut result = Value::Nil;
for form in forms {
result =
cljrs_runtime::interp::eval::eval(&form, env).map_err(|e| format!("eval: {e:?}"))?;
}
Ok(format!("{result}"))
}
fn is_true(env: &mut Env, src: &str) -> bool {
match eval_in(env, src) {
Ok(s) => s == "true",
Err(e) => panic!("{src}\n{e}"),
}
}
fn outcome(env: &mut Env, observer: &str, value: &str) -> String {
match eval_in(env, &format!("({observer} {value})")) {
Ok(v) => format!("ok:{v}"),
Err(e) => {
let e = e.replace('\n', " ");
format!("err:{}", e.split("message:").last().unwrap_or(&e).trim())
}
}
}
#[derive(Clone, Debug)]
struct Annotation {
src: String,
expected: String,
}
const RESERVED: &[&str] = &["async", "tag", "private", "dynamic", "macro", "const"];
fn name_strategy() -> impl Strategy<Value = String> {
"[a-z]{1,5}".prop_filter("reserved annotation key", |s: &String| {
!RESERVED.contains(&s.as_str())
})
}
fn scalar_strategy() -> impl Strategy<Value = String> {
prop_oneof![
(-1000i64..1000).prop_map(|n| n.to_string()),
name_strategy().prop_map(|s| format!(":{s}")),
name_strategy().prop_map(|s| format!("\"{s}\"")),
Just("true".to_string()),
Just("nil".to_string()),
]
}
fn annotation_strategy() -> impl Strategy<Value = Annotation> {
prop_oneof![
name_strategy().prop_map(|k| Annotation {
src: format!(":{k}"),
expected: format!("{{:{k} true}}"),
}),
"[A-Z][a-z]{0,4}".prop_map(|t| Annotation {
src: t.clone(),
expected: format!("{{:tag '{t}}}"),
}),
name_strategy().prop_map(|t| Annotation {
src: format!("\"{t}\""),
expected: format!("{{:tag \"{t}\"}}"),
}),
(name_strategy(), scalar_strategy()).prop_map(|(k, v)| Annotation {
src: format!("{{:{k} {v}}}"),
expected: format!("{{:{k} {v}}}"),
}),
]
}
fn constructing_form_strategy() -> impl Strategy<Value = String> {
prop_oneof![
Just("[]".to_string()),
Just("[1 2 3]".to_string()),
Just("{:k 1}".to_string()),
Just("#{1 2}".to_string()),
Just("(fn [] 1)".to_string()),
Just("(fn [x] x)".to_string()),
Just("#(inc %)".to_string()),
Just("[[1] {:k 1}]".to_string()),
]
}
fn hint_form_strategy() -> impl Strategy<Value = String> {
prop_oneof![
Just("(list 1)".to_string()),
Just("(vector 1)".to_string()),
Just("(if true [1] [2])".to_string()),
Just("(do [1])".to_string()),
Just("(let [q [1]] q)".to_string()),
Just("'(1 2)".to_string()),
Just("'sym".to_string()),
Just("42".to_string()),
Just("\"s\"".to_string()),
Just(":kw".to_string()),
]
}
const OBSERVERS: &[&str] = &[
"vector?",
"map?",
"set?",
"coll?",
"seq?",
"fn?",
"ifn?",
"sequential?",
"associative?",
"counted?",
"empty?",
"some?",
"nil?",
"number?",
"string?",
"keyword?",
"symbol?",
"boolean?",
"type",
"count",
];
proptest! {
#![proptest_config(ProptestConfig::with_cases(96))]
#[test]
fn a_constructing_form_carries_exactly_the_annotation(
ann in annotation_strategy(),
form in constructing_form_strategy(),
) {
let (_g, mut env) = make_env();
let Annotation { src, expected } = ann;
prop_assert!(
is_true(&mut env, &format!("(= (meta ^{src} {form}) {expected})")),
"^{src} {form} did not carry {expected}"
);
}
#[test]
fn a_hint_form_carries_nothing(
ann in annotation_strategy(),
form in hint_form_strategy(),
) {
let (_g, mut env) = make_env();
let src = ann.src;
prop_assert!(
is_true(&mut env, &format!("(nil? (meta ^{src} {form}))")),
"^{src} {form} carried metadata; a hint position must carry none"
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(96))]
#[test]
fn an_annotation_never_changes_the_value(
ann in annotation_strategy(),
form in constructing_form_strategy(),
) {
let (_g, mut env) = make_env();
let src = ann.src;
prop_assert!(
is_true(&mut env, &format!("(= (type ^{src} {form}) (type {form}))")),
"^{src} {form} changed its type"
);
if !form.starts_with("(fn") && !form.starts_with('#') || form.starts_with("#{") {
prop_assert!(
is_true(&mut env, &format!("(= ^{src} {form} {form})")),
"^{src} {form} is no longer equal to {form}"
);
prop_assert!(
is_true(&mut env, &format!("(= (hash ^{src} {form}) (hash {form}))")),
"^{src} {form} hashes differently from {form}"
);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(96))]
#[test]
fn stacked_annotations_merge_outer_last(
outer in annotation_strategy(),
inner in annotation_strategy(),
form in constructing_form_strategy(),
) {
let (_g, mut env) = make_env();
let expr = format!("(meta ^{} ^{} {form})", outer.src, inner.src);
let law = format!("(merge {} {})", inner.expected, outer.expected);
prop_assert!(
is_true(&mut env, &format!("(= {expr} {law})")),
"^{} ^{} {form} is not (merge inner outer)", outer.src, inner.src
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(96))]
#[test]
fn a_shorthand_expands_the_same_quoted_and_evaluated(
ann in annotation_strategy(),
) {
let (_g, mut env) = make_env();
let src = ann.src;
prop_assume!(!src.starts_with('{'));
prop_assert!(
is_true(&mut env, &format!("(= (meta '^{src} [1]) (meta ^{src} [1]))")),
"^{src} expanded differently quoted vs evaluated"
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
fn no_observer_can_see_an_annotation(
ann in annotation_strategy(),
form in constructing_form_strategy(),
observer in proptest::sample::select(OBSERVERS),
) {
let (_g, mut env) = make_env();
let src = ann.src;
let annotated = outcome(&mut env, observer, &format!("^{src} {form}"));
let bare = outcome(&mut env, observer, &form);
prop_assert_eq!(
&annotated, &bare,
"`{}` told ^{} {} apart from {}", observer, src, form, form
);
}
#[test]
fn no_observer_can_see_programmatic_metadata(
ann in annotation_strategy(),
observer in proptest::sample::select(OBSERVERS),
form in prop_oneof![
Just("[1 2]".to_string()),
Just("{:k 1}".to_string()),
Just("#{1}".to_string()),
Just("'(1 2)".to_string()),
Just("'sym".to_string()),
Just(":kw".to_string()),
Just("\"s\"".to_string()),
Just("1".to_string()),
Just("nil".to_string()),
],
) {
let (_g, mut env) = make_env();
let expected = ann.expected;
let annotated = outcome(&mut env, observer, &format!("(with-meta {form} {expected})"));
let bare = outcome(&mut env, observer, &form);
prop_assert_eq!(
&annotated, &bare,
"`{}` told (with-meta {} …) apart from {}", observer, form, form
);
}
}