Skip to content

Commit c32d8ab

Browse files
authored
fix SPECIFICATION directive extracting only first init conjunct (#18)
* fix SPECIFICATION directive extracting only first init conjunct (#17) * simplify collect_init and use direct PartialEq in test * bump version to 0.3.4 and update changelog
1 parent 5636c0d commit c32d8ab

6 files changed

Lines changed: 155 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [0.3.4] - 2026-03-08
4+
5+
### Fixed
6+
7+
- `SPECIFICATION` cfg directive extracting only the first init conjunct, producing 0 initial states for multi-variable specs
8+
39
## [0.3.3] - 2026-02-25
410

511
### Added

Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tla-checker"
3-
version = "0.3.3"
3+
version = "0.3.4"
44
edition = "2024"
55
description = "A TLA+ model checker written in Rust"
66
license = "MIT OR Apache-2.0"
@@ -33,7 +33,6 @@ strip = true
3333
panic = "abort"
3434

3535
[profile.bench]
36-
panic = "unwind"
3736
strip = false
3837

3938
[profile.profiling]

src/config.rs

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -615,18 +615,23 @@ fn find_box_action(expr: &Expr) -> Option<Expr> {
615615
}
616616
}
617617

618-
fn leftmost_conjunct(expr: &Expr) -> &Expr {
618+
fn collect_init(expr: &Expr) -> Option<Expr> {
619619
match expr {
620-
Expr::And(l, _) => leftmost_conjunct(l),
621-
other => other,
620+
Expr::BoxAction(_, _) => None,
621+
Expr::And(l, r) => match (collect_init(l), collect_init(r)) {
622+
(Some(a), Some(b)) => Some(Expr::And(Box::new(a), Box::new(b))),
623+
(a, None) => a,
624+
(None, b) => b,
625+
},
626+
other => Some(other.clone()),
622627
}
623628
}
624629

625630
fn resolve_specification(spec_name: &Arc<str>, spec: &mut Spec) -> Result<(), String> {
626631
match spec.definitions.get(spec_name.as_ref()) {
627632
Some((params, expr)) if params.is_empty() => {
628633
if let Some(next_expr) = find_box_action(expr) {
629-
spec.init = Some(leftmost_conjunct(expr).clone());
634+
spec.init = Some(collect_init(expr).unwrap_or(Expr::Lit(Value::Bool(true))));
630635
spec.next = Some(next_expr);
631636
return Ok(());
632637
}
@@ -1001,6 +1006,65 @@ mod tests {
10011006
);
10021007
}
10031008

1009+
#[test]
1010+
fn apply_config_specification_inlined_multi_var() {
1011+
let mut spec = Spec {
1012+
vars: vec![Arc::from("x"), Arc::from("y")],
1013+
init: None,
1014+
next: None,
1015+
invariants: vec![],
1016+
invariant_names: vec![],
1017+
definitions: std::collections::BTreeMap::new(),
1018+
instances: vec![],
1019+
extends: vec![],
1020+
fairness: vec![],
1021+
assumes: vec![],
1022+
liveness_properties: vec![],
1023+
constants: vec![],
1024+
};
1025+
1026+
let init_part_1 = Expr::In(
1027+
Box::new(Expr::Var(Arc::from("x"))),
1028+
Box::new(Expr::SetEnum(vec![Expr::Lit(Value::Int(1))])),
1029+
);
1030+
let init_part_2 = Expr::In(
1031+
Box::new(Expr::Var(Arc::from("y"))),
1032+
Box::new(Expr::SetEnum(vec![Expr::Lit(Value::Bool(true))])),
1033+
);
1034+
let next_expr = Expr::Var(Arc::from("SomeNext"));
1035+
let spec_body = Expr::And(
1036+
Box::new(Expr::And(
1037+
Box::new(init_part_1.clone()),
1038+
Box::new(init_part_2.clone()),
1039+
)),
1040+
Box::new(Expr::BoxAction(
1041+
Box::new(next_expr.clone()),
1042+
Arc::from("vars"),
1043+
)),
1044+
);
1045+
spec.definitions
1046+
.insert(Arc::from("Spec"), (vec![], spec_body));
1047+
1048+
let cfg = parse_cfg("SPECIFICATION Spec").unwrap();
1049+
let mut domains = Env::new();
1050+
let mut checker_config = CheckerConfig::default();
1051+
1052+
apply_config(
1053+
&cfg,
1054+
&mut spec,
1055+
&mut domains,
1056+
&mut checker_config,
1057+
&[],
1058+
&[],
1059+
false,
1060+
)
1061+
.unwrap();
1062+
1063+
let expected_init = Expr::And(Box::new(init_part_1), Box::new(init_part_2));
1064+
assert_eq!(spec.init.unwrap(), expected_init);
1065+
assert_eq!(spec.next.unwrap(), next_expr);
1066+
}
1067+
10041068
#[test]
10051069
fn apply_config_specification_missing_box_action_errors() {
10061070
let mut spec = Spec {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
SPECIFICATION Spec
2+
INVARIANT TypeOK
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---- MODULE specification_directive_multi_var ----
2+
LOCAL INSTANCE Integers
3+
4+
VARIABLES number, toggle
5+
6+
vars == <<number, toggle>>
7+
8+
Numbers == {22, 23}
9+
NumberMax == 23
10+
NumberMin == 22
11+
12+
Init ==
13+
/\ number \in Numbers
14+
/\ toggle \in BOOLEAN
15+
16+
NumberIncrease ==
17+
/\ number < NumberMax
18+
/\ number' = number + 1
19+
/\ UNCHANGED toggle
20+
21+
NumberDecrease ==
22+
/\ number > NumberMin
23+
/\ number' = number - 1
24+
/\ UNCHANGED toggle
25+
26+
Toggle ==
27+
/\ toggle' = ~toggle
28+
/\ UNCHANGED number
29+
30+
Next ==
31+
\/ NumberIncrease
32+
\/ NumberDecrease
33+
\/ Toggle
34+
35+
Spec ==
36+
/\ Init
37+
/\ [][Next]_vars
38+
39+
TypeOK ==
40+
/\ number \in Numbers
41+
/\ toggle \in BOOLEAN
42+
43+
====

tests/oracle.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,41 @@ fn test_should_pass_specification_directive() {
705705
);
706706
}
707707

708+
#[test]
709+
fn test_should_pass_specification_directive_multi_var() {
710+
let path = Path::new("test_cases/should_pass/specification_directive_multi_var.tla");
711+
let cfg_path = Path::new("test_cases/should_pass/specification_directive_multi_var.cfg");
712+
let input = fs::read_to_string(path).expect("failed to read spec file");
713+
let cfg_input = fs::read_to_string(cfg_path).expect("failed to read cfg file");
714+
let mut spec = parse(&input).expect("failed to parse spec");
715+
let tlc_cfg = parse_cfg(&cfg_input).expect("failed to parse cfg");
716+
717+
let mut domains = Env::new();
718+
let mut config = CheckerConfig::default();
719+
config.spec_path = Some(path.to_path_buf());
720+
721+
apply_config(
722+
&tlc_cfg,
723+
&mut spec,
724+
&mut domains,
725+
&mut config,
726+
&[],
727+
&[],
728+
false,
729+
)
730+
.expect("failed to apply config");
731+
732+
let result = check(&spec, &domains, &config);
733+
assert!(
734+
matches!(result, CheckResult::Ok(_)),
735+
"specification_directive_multi_var.tla with SPECIFICATION directive should pass, got: {result:?}",
736+
);
737+
if let CheckResult::Ok(stats) = &result {
738+
assert_eq!(stats.states_explored, 4);
739+
assert_eq!(stats.transitions, 8);
740+
}
741+
}
742+
708743
#[test]
709744
fn test_cfg_cli_constant_overrides_cfg() {
710745
let path = Path::new("test_cases/official/TwoPhase.tla");

0 commit comments

Comments
 (0)