use tla_syntax::{Expr, Unit, parse_module};
fn module(body: &str) -> tla_syntax::Module {
let src = format!("---- MODULE T ----\n{body}\n====================");
parse_module(&src).unwrap_or_else(|e| panic!("{e}\nin:\n{src}"))
}
fn definition(body: &str, name: &str) -> Expr {
module(body)
.definition(name)
.unwrap_or_else(|| panic!("`{name}` is not defined"))
.body
.clone()
}
#[test]
fn prose_outside_the_module_is_not_lexed() {
let src = "\
Notes: this spec is Bob\u{27}s; see \u{201c}the paper\u{201d} for why.
It costs ~5\u{20ac} to run \u{2014} worth it.
---- MODULE T ----
Foo == 1
====================
Afterwards: \u{201c}done\u{201d}, said Bob\u{27}s colleague.";
let m = parse_module(src).expect("the prose is ignored");
assert_eq!(m.name, "T");
assert_eq!(m.units.len(), 1);
}
#[test]
fn an_infix_operator_can_be_defined() {
let m = module("a \\prec b == a < b");
let def = m.definition("\\prec").expect("the operator is defined");
let names: Vec<&str> = def.params.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names, ["a", "b"]);
}
#[test]
fn prefix_and_postfix_operators_can_be_defined() {
let m = module("-. a == 0 - a\nb ^+ == b + 1");
assert!(m.definition("-").is_some(), "prefix minus");
assert!(m.definition("^+").is_some(), "postfix plus");
}
#[test]
fn a_postfix_operator_binds_to_what_precedes_it() {
let body = definition("Foo == S^+ \\cup T", "Foo");
assert!(
matches!(&body, Expr::Binary(_, lhs, _) if matches!(**lhs, Expr::Unary(..))),
"{body}"
);
}
#[test]
fn a_function_may_be_defined_by_its_graph() {
let body = definition("f[x \\in S] == x + 1", "f");
assert!(matches!(body, Expr::FnDef { .. }), "{body}");
}
#[test]
fn case_arms_are_separated_by_boxes() {
let body = definition(
r#"Colour == CASE n = 1 -> "red" [] n = 2 -> "green" [] OTHER -> "grey""#,
"Colour",
);
let Expr::Case { arms, other } = &body else {
panic!("expected a CASE, got {body}");
};
assert_eq!(arms.len(), 2);
assert!(other.is_some());
}
#[test]
fn an_operator_may_follow_a_bulleted_list() {
let body = definition(
"Safe ==\n /\\ TypeOK\n /\\ OneVote\n => Consistent",
"Safe",
);
assert!(
matches!(&body, Expr::Binary(op, ..) if *op == tla_syntax::token::Op::Implies),
"{body}"
);
}
#[test]
fn higher_order_parameters_record_their_arity() {
let m = module("Fold(op(_, _), base, S) == base");
let def = m.definition("Fold").expect("defined");
let arities: Vec<usize> = def.params.iter().map(|p| p.arity).collect();
assert_eq!(arities, [2, 0, 0]);
}
#[test]
fn a_lambda_may_be_passed_where_an_operator_is_wanted() {
let body = definition("Foo == Apply(LAMBDA x, y : x + y, 1)", "Foo");
let Expr::Apply(_, args) = &body else {
panic!("expected an application, got {body}");
};
assert!(matches!(args[0], Expr::Lambda { .. }), "{}", args[0]);
}
#[test]
fn an_operator_may_be_passed_as_an_argument() {
let body = definition("Sum(S) == FoldSet(+, 0, S)", "Sum");
let Expr::Apply(_, args) = &body else {
panic!("expected an application, got {body}");
};
assert_eq!(args[0], Expr::Ident("+".to_string()));
}
#[test]
fn temporal_quantifiers_parse_and_are_marked_as_such() {
let body = definition("Hidden == \\EE t : Timer(t)", "Hidden");
let Expr::Quant { kind, .. } = &body else {
panic!("expected a quantifier, got {body}");
};
assert!(kind.is_temporal());
}
#[test]
fn an_action_subscript_is_recognised_past_an_inner_colon() {
let body = definition("Keeps == [][\\A i \\in Proc : p[i] = p'[i]]_vars", "Keeps");
let Expr::Unary(_, inner) = &body else {
panic!("expected [] applied to something, got {body}");
};
assert!(matches!(**inner, Expr::ActionBox { .. }), "{inner}");
}
#[test]
fn a_fairness_subscript_may_be_a_tuple() {
let body = definition("F == WF_<<a, b>>(Next)", "F");
let Expr::Fairness { subscript, .. } = &body else {
panic!("expected a fairness condition, got {body}");
};
assert!(matches!(**subscript, Expr::Tuple(_)), "{subscript}");
}
#[test]
fn a_choose_inside_braces_is_not_a_set_filter() {
let body = definition("Pick == {CHOOSE x \\in S : TRUE}", "Pick");
let Expr::SetEnum(items) = &body else {
panic!("expected a one-element set, got {body}");
};
assert!(matches!(items[0], Expr::Choose { .. }), "{}", items[0]);
}
#[test]
fn set_filters_and_set_maps_are_told_apart() {
assert!(matches!(
definition("A == {x \\in S : P(x)}", "A"),
Expr::SetFilter { .. }
));
assert!(matches!(
definition("B == {f[x] : x \\in S}", "B"),
Expr::SetMap { .. }
));
}
#[test]
fn except_accepts_several_indices_at_once() {
let body = definition("Set == [f EXCEPT ![a, b] = v]", "Set");
assert!(matches!(body, Expr::Except { .. }), "{body}");
}
#[test]
fn a_named_theorem_keeps_its_statement() {
let m = module("THEOREM Correct == Spec => []Inv");
assert!(m.units.iter().any(|u| matches!(u, Unit::Theorem(_))));
}
#[test]
fn a_proof_is_skipped_and_the_module_continues() {
let m = module(
"THEOREM Correct == Spec => []Inv\n\
<1>1. Init => Inv\n\
BY DEF Init, Inv\n\
<1>2. QED\n\
BY <1>1\n\
After == 42",
);
assert!(
m.definition("After").is_some(),
"the definition after the proof is still read"
);
}
#[test]
fn a_module_may_contain_another() {
let m = module("---- MODULE Inner ----\nX == 1\n====================\nOuter == 2");
assert!(m.units.iter().any(|u| matches!(u, Unit::Inner(_))));
assert!(m.definition("Outer").is_some());
}
#[test]
fn labels_are_read_and_dropped() {
let body = definition("Step == /\\ Lbl:: x = 1\n /\\ y = 2", "Step");
assert_eq!(body.to_string(), "x = 1 /\\ y = 2");
}
#[test]
fn instance_names_may_be_chained_and_take_arguments() {
let body = definition("Q == A!B!C", "Q");
let Expr::Qualified { instance, name, .. } = &body else {
panic!("expected a qualified name, got {body}");
};
assert_eq!((instance.as_str(), name.as_str()), ("A!B", "C"));
}
#[test]
fn a_name_may_begin_with_a_digit_or_an_underscore() {
let m = module("1aMessage == 1\n_hidden == 2");
assert!(m.definition("1aMessage").is_some());
assert!(m.definition("_hidden").is_some());
}
#[test]
fn the_new_forms_round_trip() {
for source in [
"S^+ \\cup T",
"CASE n = 1 -> \"a\" [] OTHER -> \"b\"",
"\\EE t : Timer(t)",
"WF_<<a, b>>(Next)",
"{CHOOSE x \\in S : TRUE}",
"[f EXCEPT ![a, b] = v]",
"A!B!C",
"Apply(LAMBDA x, y : x + y, 1)",
"[][\\A i \\in Proc : p[i] = p'[i]]_vars",
] {
let parsed = tla_syntax::parse_expression(source)
.unwrap_or_else(|e| panic!("parsing `{source}`: {e}"));
let printed = parsed.to_string();
let again = tla_syntax::parse_expression(&printed)
.unwrap_or_else(|e| panic!("re-reading `{printed}`: {e}"));
assert_eq!(parsed, again, "`{source}` printed as `{printed}`");
}
}
#[test]
fn operators_can_be_declared_by_their_fixity() {
let m = module("CONSTANTS _+_, -._, _^#, Plain\nUse(_*_, x) == x");
let declared: Vec<(&str, usize)> = m.constants().map(|d| (d.name.as_str(), d.arity)).collect();
assert_eq!(declared, [("+", 2), ("-", 1), ("^#", 1), ("Plain", 0)]);
let params = &m.definition("Use").expect("defined").params;
assert_eq!((params[0].name.as_str(), params[0].arity), ("*", 2));
}
#[test]
fn numbers_may_be_written_in_another_base() {
assert_eq!(definition("X == \\b1011", "X"), Expr::Num(11));
assert_eq!(definition("X == \\o777", "X"), Expr::Num(511));
assert_eq!(definition("X == \\h1F", "X"), Expr::Num(31));
assert!(matches!(
definition("X == a \\o b", "X"),
Expr::Binary(tla_syntax::token::Op::Concat, ..)
));
}
#[test]
fn a_decimal_is_kept_as_written() {
assert_eq!(
definition("X == 000123.456000", "X"),
Expr::Decimal("000123.456000".to_string())
);
assert!(matches!(
definition("X == 1..2", "X"),
Expr::Binary(tla_syntax::token::Op::DotDot, ..)
));
}
#[test]
fn a_field_may_be_spelled_like_a_keyword() {
assert_eq!(definition("X == bar.NEW", "X").to_string(), "bar.NEW");
assert_eq!(definition("X == bar.SF_", "X").to_string(), "bar.SF_");
assert_eq!(
definition("X == [f EXCEPT !.DOMAIN = 1]", "X").to_string(),
"[f EXCEPT !.DOMAIN = 1]"
);
}
#[test]
fn a_let_may_introduce_an_instance() {
let body = definition("X == LET I == INSTANCE M IN I!Op", "X");
let Expr::Let { instances, .. } = &body else {
panic!("expected a LET, got {body}");
};
assert_eq!(instances.len(), 1);
assert_eq!(instances[0].name.as_deref(), Some("I"));
assert_eq!(instances[0].module, "M");
}
#[test]
fn operators_can_be_passed_by_symbol() {
for (source, expected) in [
("X == apply(', v)", "'"),
("X == apply(ENABLED, v)", "ENABLED"),
("X == apply(\\cup, v)", "\\cup"),
] {
let Expr::Apply(_, args) = definition(source, "X") else {
panic!("expected an application in `{source}`");
};
assert_eq!(args[0], Expr::Ident(expected.to_string()), "{source}");
}
}
#[test]
fn an_expression_may_continue_into_the_first_column() {
let m = module("X == a\n\\/ b\n\\/ c\nY == 2");
assert_eq!(
m.definition("X").expect("X").body.to_string(),
"a \\/ b \\/ c"
);
assert!(
m.definition("Y").is_some(),
"the next definition still starts"
);
}
#[test]
fn a_prefix_operator_definition_still_starts_a_unit() {
let m = module("X == a\n-. b == 0 - b");
assert_eq!(m.definition("X").expect("X").body.to_string(), "a");
assert!(
m.definition("-").is_some(),
"the prefix definition is its own unit"
);
}
#[test]
fn a_proof_step_does_not_look_like_a_comparison() {
let m = module(
"THEOREM T == Spec => []Inv\n\
<1>1 Init => Inv\n\
BY DEF Init\n\
<1> QED\n\
After == 1",
);
assert!(m.definition("After").is_some(), "the proof was skipped");
assert_eq!(definition("X == a < 1 > b", "X").to_string(), "a < 1 > b");
}
#[test]
fn a_unit_that_reads_nothing_is_an_error_not_a_hang() {
let err = parse_module("---- MODULE T ----\n)\n====").expect_err("cannot be read");
assert!(!err.message.is_empty());
}