use tla_syntax::{Expr, LetInstance, Unit, parse_expression, parse_module};
#[track_caller]
fn after(rest: &str) -> String {
let src = format!("---- MODULE T ----\nX == a\n{rest}\n====");
let module = parse_module(&src).unwrap_or_else(|e| panic!("{e}\nin:\n{src}"));
module
.definition("X")
.unwrap_or_else(|| panic!("X is gone\nin:\n{src}"))
.body
.to_string()
}
#[track_caller]
fn defines(rest: &str, name: &str) -> bool {
let src = format!("---- MODULE T ----\nX == a\n{rest}\n====");
parse_module(&src).is_ok_and(|m| m.definition(name).is_some())
}
#[test]
fn a_definition_of_any_shape_ends_the_previous_one() {
for (rest, name) in [
("Y == 1", "Y"),
("Y(p) == p", "Y"),
("Y(p, q) == p", "Y"),
("Y(f(_)) == 1", "Y"),
("Y[i \\in S] == i", "Y"),
("Y[i, j \\in S] == i", "Y"),
("p ++ q == p", "++"),
("p ^+ == p", "^+"),
("-. p == p", "-"),
] {
assert_eq!(after(rest), "a", "`{rest}` should end X");
assert!(defines(rest, name), "`{rest}` should define {name}");
}
}
#[test]
fn a_declaration_ends_the_definition_before_it() {
for rest in ["VARIABLE v", "CONSTANT c", "THEOREM TRUE", "ASSUME TRUE"] {
assert_eq!(after(rest), "a", "`{rest}` should end X");
}
}
#[test]
fn an_expression_wrapped_into_the_first_column_carries_on() {
for (rest, expected) in [
("\\/ b", "a \\/ b"),
("+ b", "a + b"),
("\\cup b", "a \\cup b"),
("= b", "a = b"),
] {
assert_eq!(after(rest), expected, "`{rest}` continues the expression");
}
}
#[test]
fn a_later_definitions_equals_does_not_end_this_one() {
let src = "---- MODULE T ----\nX == a\n\\/ b\n\\/ c\nY == 2\n====";
let m = parse_module(src).expect("parses");
assert_eq!(
m.definition("X").expect("X").body.to_string(),
"a \\/ b \\/ c"
);
assert_eq!(m.definition("Y").expect("Y").body.to_string(), "2");
}
#[test]
fn a_bracketed_left_hand_side_is_found_past_its_brackets() {
for (rest, name) in [
("Y[i \\in [j \\in S |-> j]] == i", "Y"),
("Y(p, f(_), q) == p", "Y"),
] {
assert_eq!(after(rest), "a", "`{rest}`");
assert!(defines(rest, name), "`{rest}`");
}
assert_eq!(after("(b)"), "a(b)");
}
#[test]
fn a_definition_is_written_with_operands_not_holes() {
for source in [
"_ ** _ == 1",
"_ ^# == 1",
"Y(f(g(_))) == 1",
"Y(<<1, 2>>) == 1",
] {
let src = format!("---- MODULE T ----\n{source}\n====");
assert!(
parse_module(&src).is_err(),
"`{source}` is not TLA+ and SANY rejects it too"
);
}
}
#[test]
fn a_prefix_definition_needs_a_name_or_a_hole() {
assert!(defines("-. a == 0 - a", "-"));
assert!(defines("- a == 0 - a", "-"));
assert_eq!(after("- b"), "a - b", "with no `==` it is a subtraction");
}
#[test]
fn a_substitution_may_be_an_operator_or_an_expression() {
let m = parse_module(
"---- MODULE T ----\nI == INSTANCE M WITH F <- ', G <- ENABLED, H <- a + b\n====",
)
.expect("parses");
let Unit::Instance { subs, .. } = &m.units[0] else {
panic!("expected an instance");
};
assert_eq!(subs[0].1, Expr::Ident("'".to_string()));
assert_eq!(subs[1].1, Expr::Ident("ENABLED".to_string()));
assert!(matches!(subs[2].1, Expr::Binary(..)), "{}", subs[2].1);
}
#[test]
fn an_operator_may_be_the_last_argument() {
let body = parse_module("---- MODULE T ----\nX == apply(v, +)\n====")
.expect("parses")
.definition("X")
.expect("X")
.body
.clone();
let Expr::Apply(_, args) = &body else {
panic!("expected an application, got {body}");
};
assert_eq!(args[1], Expr::Ident("+".to_string()));
}
#[test]
fn a_definition_may_be_named_after_a_keyword() {
for (source, name) in [
("TRUE == FALSE", "TRUE"),
("DOMAIN == 1", "DOMAIN"),
("SUBSET == 2", "SUBSET"),
("UNCHANGED == 3", "UNCHANGED"),
("LAMBDA == 4", "LAMBDA"),
] {
let src = format!("---- MODULE T ----\n{source}\n====");
let m = parse_module(&src).unwrap_or_else(|e| panic!("{source}: {e}"));
assert!(m.definition(name).is_some(), "{source} defines {name}");
}
assert!(
parse_module("---- MODULE T ----\nTRUE\n====").is_err(),
"a bare keyword is not a unit"
);
}
#[test]
fn a_let_instance_survives_printing() {
for source in [
"LET I == INSTANCE M IN I!Op",
"LET I == INSTANCE M WITH a <- 1, b <- 2 IN I!Op",
"LET INSTANCE M IN Op",
"LET d == 1 I == INSTANCE M IN I!Op",
] {
let parsed = parse_expression(source).unwrap_or_else(|e| panic!("parsing `{source}`: {e}"));
let printed = parsed.to_string();
let again =
parse_expression(&printed).unwrap_or_else(|e| panic!("re-reading `{printed}`: {e}"));
assert_eq!(parsed, again, "`{source}` printed as `{printed}`");
assert!(printed.contains("INSTANCE M"), "{printed}");
}
}
#[test]
fn a_let_instance_keeps_its_substitutions() {
let Expr::Let { instances, .. } =
parse_expression("LET I == INSTANCE M WITH a <- 1 IN I!Op").expect("parses")
else {
panic!("expected a LET");
};
let LetInstance { name, module, subs } = &instances[0];
assert_eq!(name.as_deref(), Some("I"));
assert_eq!(module, "M");
assert_eq!(subs.len(), 1);
assert_eq!(subs[0].0, "a");
}
#[test]
fn an_operator_may_be_the_last_substitution() {
let m = parse_module("---- MODULE T ----\nI == INSTANCE M WITH F <- '\nY == 1\n====")
.expect("parses");
let Unit::Instance { subs, .. } = &m.units[0] else {
panic!("expected an instance, got {:?}", m.units[0]);
};
assert_eq!(subs.len(), 1);
assert_eq!(subs[0].1, Expr::Ident("'".to_string()));
assert!(m.definition("Y").is_some(), "the next unit still starts");
}
#[test]
fn each_word_operator_can_be_passed_by_name() {
for word in ["ENABLED", "UNCHANGED", "DOMAIN", "SUBSET", "UNION"] {
let src = format!("---- MODULE T ----\nX == apply({word}, v)\n====");
let body = parse_module(&src)
.unwrap_or_else(|e| panic!("{word}: {e}"))
.definition("X")
.expect("X")
.body
.clone();
let Expr::Apply(_, args) = &body else {
panic!("expected an application for {word}, got {body}");
};
assert_eq!(args[0], Expr::Ident(word.to_string()), "{word}");
}
}
#[test]
fn a_proof_ends_at_the_next_definition_of_any_shape() {
for (rest, name) in [
("After == 1", "After"),
("After(p) == p", "After"),
("After[i \\in S] == i", "After"),
("a ++ b == a", "++"),
("a ^+ == a", "^+"),
("-. a == a", "-"),
] {
let src = format!(
"---- MODULE T ----\nTHEOREM T1 == TRUE\n<1>1. TRUE\n BY DEF T1\n{rest}\n===="
);
let m = parse_module(&src).unwrap_or_else(|e| panic!("{rest}: {e}"));
assert!(m.definition(name).is_some(), "`{rest}` follows the proof");
}
}