use std::collections::BTreeMap;
use tla_eval::{Evaluator, Modules, Spec, State, Value};
struct Sources(BTreeMap<&'static str, &'static str>);
impl Modules for Sources {
fn source(&self, name: &str) -> Option<String> {
self.0.get(name).map(|s| (*s).to_string())
}
}
fn spec(src: &str) -> Spec {
Spec::parse(src).unwrap_or_else(|e| panic!("{e}\nin:\n{src}"))
}
fn evaluator<'a>(spec: &'a Spec, constants: &[(&str, Value)]) -> Evaluator<'a> {
let map = constants
.iter()
.map(|(k, v)| ((*k).to_string(), v.clone()))
.collect();
Evaluator::new(spec, map).expect("the constants cover the declarations")
}
fn state(pairs: &[(&str, i64)]) -> State {
pairs
.iter()
.map(|(k, v)| ((*k).to_string(), Value::Int(*v)))
.collect()
}
const OPS: &str = r"---- MODULE Ops ----
EXTENDS Naturals
VARIABLE x
a \prec b == a < b
-. a == 0 - a
b ^+ == b + 1
\* `-.` is how a prefix definition is written; the use is plain `-x`.
Small == x \prec 5
Negated == -x
Bumped == x^+
====================";
#[test]
fn an_infix_operator_the_spec_defines_is_evaluated() {
let m = spec(OPS);
let e = evaluator(&m, &[]);
assert_eq!(
e.value_of("Small", &state(&[("x", 3)])).unwrap(),
Value::Bool(true)
);
assert_eq!(
e.value_of("Small", &state(&[("x", 9)])).unwrap(),
Value::Bool(false)
);
}
#[test]
fn prefix_and_postfix_definitions_are_evaluated() {
let m = spec(OPS);
let e = evaluator(&m, &[]);
let at = state(&[("x", 4)]);
assert_eq!(e.value_of("Negated", &at).unwrap(), Value::Int(-4));
assert_eq!(e.value_of("Bumped", &at).unwrap(), Value::Int(5));
}
const HIGHER_ORDER: &str = r"---- MODULE HigherOrder ----
EXTENDS Naturals
VARIABLE x
Twice(f(_), v) == f(f(v))
Combine(op(_, _), a, b) == op(a, b)
Inc(n) == n + 1
ByName == Twice(Inc, 1)
ByLambda == Twice(LAMBDA k : k * 2, 3)
BySymbol == Combine(+, 2, 3)
Nested == Combine(LAMBDA a, b : a * b, x, x)
====================";
#[test]
fn an_operator_may_be_passed_by_name() {
let m = spec(HIGHER_ORDER);
let e = evaluator(&m, &[]);
assert_eq!(
e.value_of("ByName", &state(&[("x", 0)])).unwrap(),
Value::Int(3)
);
}
#[test]
fn a_lambda_may_be_passed_and_applied() {
let m = spec(HIGHER_ORDER);
let e = evaluator(&m, &[]);
assert_eq!(
e.value_of("ByLambda", &state(&[("x", 0)])).unwrap(),
Value::Int(12)
);
}
#[test]
fn an_operator_symbol_may_be_passed() {
let m = spec(HIGHER_ORDER);
let e = evaluator(&m, &[]);
assert_eq!(
e.value_of("BySymbol", &state(&[("x", 0)])).unwrap(),
Value::Int(5)
);
}
#[test]
fn a_lambda_sees_the_state_it_was_written_in() {
let m = spec(HIGHER_ORDER);
let e = evaluator(&m, &[]);
assert_eq!(
e.value_of("Nested", &state(&[("x", 6)])).unwrap(),
Value::Int(36)
);
}
#[test]
fn a_module_may_extend_another() {
let sources = Sources(BTreeMap::from([(
"Base",
"---- MODULE Base ----\nEXTENDS Naturals\nCONSTANT Bound\nVARIABLE x\nInRange == x <= Bound\n====",
)]));
let m = Spec::load(
"---- MODULE Top ----\nEXTENDS Base\nOk == InRange /\\ x >= 0\n====",
&sources,
)
.expect("Base is found");
assert_eq!(m.variables().collect::<Vec<_>>(), ["x"]);
assert_eq!(m.constants().collect::<Vec<_>>(), ["Bound"]);
let e = evaluator(&m, &[("Bound", Value::Int(5))]);
assert_eq!(
e.value_of("Ok", &state(&[("x", 3)])).unwrap(),
Value::Bool(true)
);
assert_eq!(
e.value_of("Ok", &state(&[("x", 9)])).unwrap(),
Value::Bool(false)
);
}
#[test]
fn a_module_that_cannot_be_found_is_reported() {
let err = Spec::parse("---- MODULE Top ----\nEXTENDS Missing\nX == 1\n====")
.expect_err("Missing is not a standard module");
assert!(format!("{err}").contains("Missing"), "{err}");
}
const COMPOSED: &str = r"---- MODULE Composed ----
EXTENDS Naturals
VARIABLES a, b
---- MODULE Counter ----
EXTENDS Naturals
CONSTANT Limit
VARIABLE n
CInit == n = 0
CNext == n < Limit /\ n' = n + 1
====================
A == INSTANCE Counter WITH Limit <- 3, n <- a
B == INSTANCE Counter WITH Limit <- 5, n <- b
Init == A!CInit /\ B!CInit
Next == \/ A!CNext /\ b' = b
\/ B!CNext /\ a' = a
====================";
fn composed() -> Spec {
spec(COMPOSED)
}
#[test]
fn an_instance_substitutes_constants_and_variables() {
let m = composed();
let e = evaluator(&m, &[]);
assert!(e.holds_at("Init", &state(&[("a", 0), ("b", 0)])).unwrap());
assert!(!e.holds_at("Init", &state(&[("a", 1), ("b", 0)])).unwrap());
}
#[test]
fn priming_reaches_through_a_substitution() {
let m = composed();
let e = evaluator(&m, &[]);
let start = state(&[("a", 0), ("b", 0)]);
assert!(
e.step_allowed("Next", &start, &state(&[("a", 1), ("b", 0)]))
.unwrap()
);
assert!(
e.step_allowed("Next", &start, &state(&[("a", 0), ("b", 1)]))
.unwrap()
);
assert!(
!e.step_allowed("Next", &start, &state(&[("a", 2), ("b", 0)]))
.unwrap()
);
assert!(
!e.step_allowed("Next", &start, &state(&[("a", 1), ("b", 1)]))
.unwrap()
);
}
#[test]
fn instances_of_one_module_keep_their_own_constants() {
let m = composed();
let e = evaluator(&m, &[]);
let at = state(&[("a", 3), ("b", 3)]);
assert!(
!e.step_allowed("Next", &at, &state(&[("a", 4), ("b", 3)]))
.unwrap()
);
assert!(
e.step_allowed("Next", &at, &state(&[("a", 3), ("b", 4)]))
.unwrap()
);
}
#[test]
fn a_name_the_with_clause_omits_keeps_its_own_name() {
let sources = Sources(BTreeMap::from([(
"Inner",
"---- MODULE Inner ----\nEXTENDS Naturals\nCONSTANTS P, Q\nSum == P + Q\n====",
)]));
let m = Spec::load(
"---- MODULE Outer ----\nEXTENDS Naturals\nCONSTANT Q\nVARIABLE v\n\
I == INSTANCE Inner WITH P <- 10\nTotal == I!Sum\n====",
&sources,
)
.expect("Inner is found");
let e = evaluator(&m, &[("Q", Value::Int(7))]);
assert_eq!(
e.value_of("Total", &state(&[("v", 0)])).unwrap(),
Value::Int(17)
);
}
#[test]
fn instances_may_be_chained() {
let sources = Sources(BTreeMap::from([
(
"Leaf",
"---- MODULE Leaf ----\nEXTENDS Naturals\nCONSTANT K\nValue == K * 2\n====",
),
(
"Middle",
"---- MODULE Middle ----\nEXTENDS Naturals\nCONSTANT M\n\
L == INSTANCE Leaf WITH K <- M + 1\n====",
),
]));
let m = Spec::load(
"---- MODULE Top ----\nEXTENDS Naturals\nVARIABLE v\n\
Mid == INSTANCE Middle WITH M <- 4\nAnswer == Mid!L!Value\n====",
&sources,
)
.expect("both modules are found");
let e = evaluator(&m, &[]);
assert_eq!(
e.value_of("Answer", &state(&[("v", 0)])).unwrap(),
Value::Int(10)
);
}
#[test]
fn an_unknown_instance_is_reported_by_name() {
let m = spec("---- MODULE T ----\nVARIABLE v\nX == Nope!Thing\n====");
let e = evaluator(&m, &[]);
let err = e
.value_of("X", &state(&[("v", 0)]))
.expect_err("no such instance");
assert!(format!("{err}").contains("Nope"), "{err}");
}