├── .gitignore ├── typeset ├── _solutions │ ├── functions.txt │ ├── pairs.txt │ ├── sets.txt │ ├── records.txt │ ├── tuples.txt │ └── graph_functions.txt ├── basics │ ├── sets.tla │ ├── records.tla │ ├── tuples.tla │ ├── pairs.tla │ └── functions.tla ├── graphs │ └── graph_functions.tla ├── README.md └── check_solutions.py ├── passfail ├── _solutions │ ├── sf_counter.txt │ ├── wf_counter.txt │ ├── threads.txt │ └── criticalsection.txt ├── criticalsection │ ├── cs.cfg │ ├── cs_fail.tla │ └── cs_pass.tla ├── sf_counter │ ├── sf_counter.cfg │ ├── sf_counter_fail.tla │ └── sf_counter_pass.tla ├── threads │ ├── threads.cfg │ ├── threads_pass.tla │ └── threads_fail.tla ├── wf_counter │ ├── wf_counter.cfg │ ├── wf_counter_fail.tla │ └── wf_counter_pass.tla ├── README.md └── check_solutions.py ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | states 3 | temporary.cfg 4 | -------------------------------------------------------------------------------- /typeset/_solutions/functions.txt: -------------------------------------------------------------------------------- 1 | Typeset == [S -> T] -------------------------------------------------------------------------------- /typeset/_solutions/pairs.txt: -------------------------------------------------------------------------------- 1 | Typeset == S \X T 2 | -------------------------------------------------------------------------------- /typeset/_solutions/sets.txt: -------------------------------------------------------------------------------- 1 | Typeset == (SUBSET Elems) 2 | -------------------------------------------------------------------------------- /passfail/_solutions/sf_counter.txt: -------------------------------------------------------------------------------- 1 | Prop == <>(counter = Max) -------------------------------------------------------------------------------- /passfail/_solutions/wf_counter.txt: -------------------------------------------------------------------------------- 1 | Prop == <>(counter = Max) -------------------------------------------------------------------------------- /typeset/_solutions/records.txt: -------------------------------------------------------------------------------- 1 | Typeset == [name: Name, id: Id] 2 | -------------------------------------------------------------------------------- /typeset/_solutions/tuples.txt: -------------------------------------------------------------------------------- 1 | Typeset(N) == [1..N -> S] 2 | 3 | -------------------------------------------------------------------------------- /passfail/criticalsection/cs.cfg: -------------------------------------------------------------------------------- 1 | SPECIFICATION Spec 2 | 3 | PROPERTY Prop 4 | -------------------------------------------------------------------------------- /typeset/_solutions/graph_functions.txt: -------------------------------------------------------------------------------- 1 | Typeset == [Nodes -> SUBSET Nodes] 2 | -------------------------------------------------------------------------------- /passfail/_solutions/threads.txt: -------------------------------------------------------------------------------- 1 | \* The number of threads 2 | Prop == <>(counter = NumThreads) -------------------------------------------------------------------------------- /passfail/sf_counter/sf_counter.cfg: -------------------------------------------------------------------------------- 1 | SPECIFICATION Spec 2 | 3 | PROPERTY Prop 4 | 5 | CHECK_DEADLOCK FALSE -------------------------------------------------------------------------------- /passfail/threads/threads.cfg: -------------------------------------------------------------------------------- 1 | SPECIFICATION Spec 2 | 3 | PROPERTY Prop 4 | 5 | CHECK_DEADLOCK FALSE 6 | -------------------------------------------------------------------------------- /passfail/wf_counter/wf_counter.cfg: -------------------------------------------------------------------------------- 1 | SPECIFICATION Spec 2 | 3 | PROPERTY Prop 4 | 5 | CHECK_DEADLOCK FALSE -------------------------------------------------------------------------------- /passfail/_solutions/criticalsection.txt: -------------------------------------------------------------------------------- 1 | Prop == [](\A t1, t2 \in Threads: 2 | t1 # t2 /\ pc[t1] = "cs" => pc[t2] # "cs" 3 | ) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TLA+ Exercises 2 | 3 | Some experiments in making exercises for teaching TLA+: 4 | 5 | * Passfail: Exercises in finding useful properties. 6 | * Typeset: Exercises in generating useful sets. 7 | 8 | 9 | -------------------------------------------------------------------------------- /passfail/wf_counter/wf_counter_fail.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE wf_counter_fail ---- 2 | EXTENDS Integers 3 | 4 | VARIABLES counter 5 | vars == <> 6 | 7 | Max == 5 8 | 9 | Init == 10 | counter = 0 11 | 12 | Inc == 13 | /\ counter < Max 14 | /\ counter' = counter + 1 15 | 16 | Next == Inc 17 | 18 | Spec == Init /\ [][Next]_vars 19 | 20 | ---- 21 | 22 | Prop == FALSE \* this should FAIL 23 | 24 | ==== -------------------------------------------------------------------------------- /passfail/wf_counter/wf_counter_pass.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE wf_counter_pass ---- 2 | EXTENDS Integers 3 | 4 | VARIABLES counter 5 | vars == <> 6 | 7 | Max == 5 8 | 9 | Init == 10 | counter = 0 11 | 12 | Inc == 13 | /\ counter < Max 14 | /\ counter' = counter + 1 15 | 16 | Next == Inc 17 | 18 | Spec == Init /\ [][Next]_vars /\ WF_vars(Next) 19 | 20 | ---- 21 | 22 | Prop == TRUE \* this should PASS 23 | 24 | ==== 25 | 26 | -------------------------------------------------------------------------------- /passfail/sf_counter/sf_counter_fail.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE sf_counter_fail ---- 2 | EXTENDS Integers 3 | 4 | VARIABLES counter 5 | vars == <> 6 | 7 | Max == 5 8 | 9 | Init == 10 | counter = 0 11 | 12 | Inc == 13 | /\ counter < Max 14 | /\ counter' = counter + 1 15 | 16 | Reset == counter' = 0 17 | 18 | Next == Inc \/ Reset 19 | 20 | Fairness == 21 | /\ WF_vars(Next) 22 | /\ \A i \in 0..(Max-1): 23 | WF_vars(counter = i /\ Inc) 24 | 25 | Spec == Init /\ [][Next]_vars /\ Fairness 26 | 27 | ---- 28 | 29 | Prop == FALSE \* this should FAIL 30 | 31 | ==== -------------------------------------------------------------------------------- /passfail/sf_counter/sf_counter_pass.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE sf_counter_pass ---- 2 | EXTENDS Integers 3 | 4 | VARIABLES counter 5 | vars == <> 6 | 7 | Max == 5 8 | 9 | Init == 10 | counter = 0 11 | 12 | Inc == 13 | /\ counter < Max 14 | /\ counter' = counter + 1 15 | 16 | Reset == counter' = 0 17 | 18 | Next == Inc \/ Reset 19 | 20 | Fairness == 21 | /\ WF_vars(Next) 22 | /\ \A i \in 0..(Max-1): 23 | SF_vars(counter = i /\ Inc) 24 | 25 | Spec == Init /\ [][Next]_vars /\ Fairness 26 | 27 | ---- 28 | 29 | Prop == TRUE \* this should PASS 30 | 31 | ==== -------------------------------------------------------------------------------- /passfail/threads/threads_pass.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE threads_pass ---- 2 | EXTENDS Integers 3 | 4 | VARIABLES pc, counter 5 | vars == <> 6 | 7 | NumThreads == 5 8 | Threads == 1..5 9 | 10 | \* State transition action 11 | Trans(thread, from, to) == 12 | /\ pc[thread] = from 13 | /\ pc' = [pc EXCEPT ![thread] = to] 14 | 15 | \* Both threads start in the `start` state 16 | Init == 17 | /\ pc = [t \in Threads |-> "start"] 18 | /\ counter = 0 19 | 20 | Next == 21 | \/ \E t \in Threads: 22 | /\ Trans(t, "start", "done") 23 | /\ counter' = counter + 1 24 | 25 | Spec == Init /\ [][Next]_vars /\ WF_vars(Next) 26 | 27 | ---- 28 | 29 | Prop == TRUE \* this should PASS 30 | 31 | ==== 32 | -------------------------------------------------------------------------------- /passfail/criticalsection/cs_fail.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE cs_fail ---- 2 | EXTENDS Integers 3 | 4 | VARIABLES pc 5 | vars == <> 6 | 7 | NumThreads == 2 8 | Threads == 1..2 9 | 10 | \* State transition action 11 | Trans(thread, from, to) == 12 | /\ pc[thread] = from 13 | /\ pc' = [pc EXCEPT ![thread] = to] 14 | 15 | Init == 16 | /\ pc = [t \in Threads |-> "waiting"] 17 | 18 | EnterCriticalSection(t) == 19 | /\ Trans(t, "waiting", "cs") 20 | 21 | LeaveCriticalSection(t) == 22 | /\ Trans(t, "cs", "waiting") 23 | 24 | Next == 25 | \/ \E t \in Threads: 26 | \/ EnterCriticalSection(t) 27 | \/ LeaveCriticalSection(t) 28 | 29 | Spec == Init /\ [][Next]_vars /\ WF_vars(Next) 30 | 31 | ---- 32 | 33 | 34 | Prop == FALSE \* this should FAIL 35 | 36 | ==== 37 | -------------------------------------------------------------------------------- /typeset/basics/sets.tla: -------------------------------------------------------------------------------- 1 | Create the set of all subsets of Elems, including the empty set. 2 | 3 | ---- MODULE sets ---- 4 | EXTENDS Integers, TLC 5 | 6 | Elems == {"a", "b", "c", "d"} 7 | 8 | Typeset == {} \* Fill this in 9 | 10 | Tests == { 11 | {}, 12 | {"a"}, 13 | {"a", "d"}, 14 | {"b", "c", "d"}, 15 | Elems 16 | } 17 | 18 | Eval == 19 | \* /\ PrintT(Typeset) \* Uncomment to print set 20 | \* /\ PrintT(TLCEval(Typeset)) \* Uncomment to print all elements in set 21 | /\ LET invalid == {t \in Tests: t \notin Typeset} 22 | IN IF invalid = {} 23 | THEN PrintT("All Correct!") 24 | ELSE /\ PrintT("Incorrect. The following were not present in the set:") 25 | /\ PrintT(invalid) 26 | 27 | Spec == Eval /\ [][TRUE]_TRUE 28 | 29 | ==== 30 | -------------------------------------------------------------------------------- /passfail/criticalsection/cs_pass.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE cs_pass ---- 2 | EXTENDS Integers 3 | 4 | VARIABLES pc, lock 5 | vars == <> 6 | 7 | NumThreads == 2 8 | Threads == 1..2 9 | 10 | \* State transition action 11 | Trans(thread, from, to) == 12 | /\ pc[thread] = from 13 | /\ pc' = [pc EXCEPT ![thread] = to] 14 | 15 | Init == 16 | /\ pc = [t \in Threads |-> "waiting"] 17 | /\ lock = 0 18 | 19 | EnterCriticalSection(t) == 20 | /\ lock = 0 21 | /\ lock' = t 22 | /\ Trans(t, "waiting", "cs") 23 | 24 | LeaveCriticalSection(t) == 25 | /\ lock = t 26 | /\ lock' = 0 27 | /\ Trans(t, "cs", "waiting") 28 | 29 | Next == 30 | \/ \E t \in Threads: 31 | \/ EnterCriticalSection(t) 32 | \/ LeaveCriticalSection(t) 33 | 34 | Spec == Init /\ [][Next]_vars /\ WF_vars(Next) 35 | 36 | ---- 37 | 38 | 39 | Prop == TRUE \* this should PASS 40 | 41 | ==== 42 | -------------------------------------------------------------------------------- /passfail/threads/threads_fail.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE threads_fail ---- 2 | EXTENDS Integers 3 | 4 | VARIABLES pc, counter, tmp 5 | vars == <> 6 | 7 | NumThreads == 5 8 | Threads == 1..5 9 | 10 | \* State transition action 11 | Trans(thread, from, to) == 12 | /\ pc[thread] = from 13 | /\ pc' = [pc EXCEPT ![thread] = to] 14 | 15 | Init == 16 | /\ pc = [t \in Threads |-> "start"] 17 | /\ counter = 0 18 | /\ tmp = [t \in Threads |-> 0] 19 | 20 | GetCounter(t) == 21 | /\ tmp' = [tmp EXCEPT ![t] = counter] 22 | /\ UNCHANGED counter 23 | 24 | IncCounter(t) == 25 | /\ counter' = tmp[t] + 1 26 | /\ UNCHANGED tmp 27 | 28 | Next == 29 | \/ \E t \in Threads: 30 | \/ /\ Trans(t, "start", "inc") 31 | /\ GetCounter(t) 32 | \/ /\ Trans(t, "inc", "done") 33 | /\ IncCounter(t) 34 | 35 | Spec == Init /\ [][Next]_vars /\ WF_vars(Next) 36 | 37 | ---- 38 | 39 | 40 | Prop == FALSE \* this should FAIL 41 | ==== 42 | -------------------------------------------------------------------------------- /typeset/basics/records.tla: -------------------------------------------------------------------------------- 1 | Create the set of all records where key name is in the set Name and the key id is in the set Id. 2 | 3 | (Records are also sometimes called "structs".) 4 | 5 | ---- MODULE records ---- 6 | EXTENDS Integers, TLC 7 | 8 | Name == {"a", "b"} 9 | Id == 1..2 10 | 11 | Typeset == {} \* Fill this in 12 | 13 | Tests == { 14 | [name |-> "a", id |-> 1], 15 | [name |-> "a", id |-> 2], 16 | [name |-> "b", id |-> 1], 17 | [name |-> "b", id |-> 2] 18 | } 19 | 20 | Eval == 21 | \* /\ PrintT(Typeset) \* Uncomment to print set 22 | \* /\ PrintT(TLCEval(Typeset)) \* Uncomment to print all elements in set 23 | /\ LET invalid == {t \in Tests: t \notin Typeset} 24 | IN IF invalid = {} 25 | THEN PrintT("All Correct!") 26 | ELSE /\ PrintT("Incorrect. The following were not present in the set:") 27 | /\ PrintT(invalid) 28 | 29 | Spec == Eval /\ [][TRUE]_TRUE 30 | 31 | ==== 32 | 33 | ==== 34 | -------------------------------------------------------------------------------- /typeset/graphs/graph_functions.tla: -------------------------------------------------------------------------------- 1 | Create the set of functions that each map each element of Nodes to sets of other Nodes. 2 | 3 | This is one way to represent graphs in TLA+. 4 | 5 | ---- MODULE graph_functions ---- 6 | EXTENDS Integers, TLC 7 | 8 | Nodes == {"a", "b", "c", "d"} 9 | 10 | Typeset == {} \* Fill this in 11 | 12 | Tests == { 13 | [n \in Nodes |-> {}], \* Empty graph 14 | [n \in Nodes |-> Nodes], \* Complete graph 15 | [n \in Nodes |-> {"a"}], \* All nodes have edge to a 16 | [n \in Nodes |-> {n, "a"}] \* Edge to a + self-loop 17 | } 18 | 19 | Eval == 20 | \* /\ PrintT(Typeset) \* Uncomment to print set 21 | \* /\ PrintT(TLCEval(Typeset)) \* Uncomment to print all elements in set 22 | /\ LET invalid == {t \in Tests: t \notin Typeset} 23 | IN IF invalid = {} 24 | THEN PrintT("All Correct!") 25 | ELSE /\ PrintT("Incorrect. The following were not present in the set:") 26 | /\ PrintT(invalid) 27 | 28 | Spec == Eval /\ [][TRUE]_TRUE 29 | ==== 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Hillel Wayne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /typeset/basics/tuples.tla: -------------------------------------------------------------------------------- 1 | In this exercise, Typeset is an OPERATOR that takes a number N and returns the set of length-N tuples where all values are in S. Eg, Typeset(19) should be allthe length-19 tuples. 2 | 3 | ---- MODULE tuples ---- 4 | 5 | EXTENDS Integers, TLC 6 | 7 | S == {1, 3, 5, 7} 8 | 9 | Typeset(N) == {} \* Fill this in 10 | 11 | ThreeTests == { 12 | <<1, 3, 5>>, 13 | <<5, 5, 5>>, 14 | <<7, 1, 1>> 15 | } 16 | 17 | FourTests == { 18 | <<1, 3, 1, 5>>, 19 | <<1, 3, 1, 7>>, 20 | <<5, 5, 5, 5>> 21 | } 22 | 23 | Eval == 24 | \* /\ PrintT(Typeset(3)) \* Uncomment to print set 25 | \* /\ PrintT(TLCEval(Typeset(3))) \* Uncomment to print all elements in set 26 | /\ LET 27 | invalid_three == {t \in ThreeTests: t \notin Typeset(3)} 28 | invalid_four == {t \in FourTests: t \notin Typeset(4)} 29 | IN IF invalid_three \union invalid_four = {} 30 | THEN PrintT("All Correct!") 31 | ELSE /\ PrintT("Incorrect. The following were not present in the 3-set:") 32 | /\ PrintT(invalid_three) 33 | /\ PrintT("And the following were not present in the 4-set:") 34 | /\ PrintT(invalid_four) 35 | 36 | Spec == Eval /\ [][TRUE]_TRUE 37 | ==== 38 | -------------------------------------------------------------------------------- /typeset/basics/pairs.tla: -------------------------------------------------------------------------------- 1 | Create the set of all pairs of elements where the first element is in S and the second is in T. 2 | 3 | ---- MODULE pairs ---- 4 | EXTENDS TLC 5 | 6 | S == {1, 3, 5} 7 | T == {2, 4, 6} 8 | 9 | Typeset == {} \* Fill this in 10 | 11 | PositiveTests == { 12 | <<1, 2>>, 13 | <<1, 4>>, 14 | <<3, 6>>, 15 | <<5, 2>> 16 | } 17 | 18 | NegativeTests == { 19 | <<2, 1>>, 20 | <<4, 1>>, 21 | <<6, 3>>, 22 | <<2, 5>>, 23 | <<1, 8>>, 24 | <<3, 3>> 25 | } 26 | 27 | Eval == 28 | \* /\ PrintT(Typeset) \* Uncomment to print set 29 | \* /\ PrintT(TLCEval(Typeset)) \* Uncomment to print all elements in set 30 | /\ LET 31 | invalid_positive == {t \in PositiveTests: t \notin Typeset} 32 | invalid_negative == {t \in NegativeTests: t \in Typeset} 33 | IN IF invalid_positive \union invalid_negative = {} 34 | THEN PrintT("All Correct!") 35 | ELSE /\ PrintT("Incorrect. The following were not present in the set:") 36 | /\ PrintT(invalid_positive) 37 | /\ PrintT("And the following were present in the set but shouldn't be:") 38 | /\ PrintT(invalid_negative) 39 | 40 | Spec == Eval /\ [][TRUE]_TRUE 41 | 42 | ==== 43 | -------------------------------------------------------------------------------- /typeset/basics/functions.tla: -------------------------------------------------------------------------------- 1 | Create the set of all functions that map values of S to values of T. 2 | 3 | ---- MODULE functions ---- 4 | EXTENDS TLC 5 | 6 | S == {1, 3} 7 | T == {"a", "b"} 8 | 9 | Typeset == {} \* Fill this in 10 | 11 | PositiveTests == { 12 | 1 :> "a" @@ 3 :> "a", 13 | 1 :> "a" @@ 3 :> "b", 14 | 1 :> "b" @@ 3 :> "a", 15 | 1 :> "b" @@ 3 :> "b" 16 | } 17 | 18 | NegativeTests == { 19 | 1 :> "a", 20 | 1 :> "a" @@ 3 :> "c", 21 | 1 :> "a" @@ 3 :> "b" @@ 2 :> "b" 22 | } 23 | 24 | Eval == 25 | \* /\ PrintT(Typeset) \* Uncomment to print set 26 | \* /\ PrintT(TLCEval(Typeset)) \* Uncomment to print all elements in set 27 | /\ LET 28 | invalid_positive == {t \in PositiveTests: t \notin Typeset} 29 | invalid_negative == {t \in NegativeTests: t \in Typeset} 30 | IN IF invalid_positive \union invalid_negative = {} 31 | THEN PrintT("All Correct!") 32 | ELSE /\ PrintT("Incorrect. The following were not present in the set:") 33 | /\ PrintT(invalid_positive) 34 | /\ PrintT("And the following were present in the set but shouldn't be:") 35 | /\ PrintT(invalid_negative) 36 | 37 | Spec == Eval /\ [][TRUE]_TRUE 38 | 39 | 40 | ==== 41 | -------------------------------------------------------------------------------- /typeset/README.md: -------------------------------------------------------------------------------- 1 | # Typeset exercises 2 | 3 | In these exercises, you will be asked to generate a set matching some conditions. For example, you might be presented with: 4 | 5 | ```tla 6 | --- MODULE example ---- 7 | EXTENDS Integers 8 | 9 | Typeset == {} \* all integers between 1 and 10 10 | ==== 11 | ``` 12 | 13 | In this example, one solution would be `Typeset == 1..10`. Solutions to each exercise can be found in the `_solutions` folder. 14 | 15 | Additional rules: 16 | 17 | - Each exercise is runnable with `SPECIFICATION Spec` as the `.cfg`. Exercises will test that some representative items are in the set, and optionally some items that *should not* be in the set. Tests are *not* comprehensive. 18 | - You may need to use set map and filter to make the sets. 19 | 20 | 21 | 22 | 23 | Tips: 24 | 25 | - Each exercise comes with two lines you can comment for diagnostic information. The first, `PrintT(Typeset)`, will print your set in condensed form. The second, `PrintT(TLCEval(Typeset))`, will try to print every element in your set. Just be careful, it can freeze the checker if your set is too big! 26 | 27 | ### Purpose 28 | 29 | It's good practice to write a `TypeInvariant` for all of your specs that constrains the values of your variables: 30 | 31 | ``` 32 | TypeInvariant == 33 | /\ msg \in MessageType 34 | \* etc 35 | ``` 36 | 37 | The "type" of `msg` is `MessageType`, as it's the set of values that `msg` can have. It's useful to practice defining sets for this reason. It's also helpful for making nondeterministic selections of values. 38 | -------------------------------------------------------------------------------- /passfail/README.md: -------------------------------------------------------------------------------- 1 | # Passfail exercises 2 | 3 | In this set of exercises, you will be presented with two specifications that each do the same thing. You will need to write `Prop` so that one spec **passes** the property and one spec **fails** the property. For example, you might be presented with these: 4 | 5 | 6 | ```tla 7 | ---- MODULE example_pass ---- 8 | VARIABLE x 9 | 10 | Init == x = TRUE 11 | Next == x' = TRUE 12 | Spec == Init /\ [][Next]_x 13 | ==== 14 | 15 | ---- MODULE example_fail ---- 16 | VARIABLE x 17 | 18 | Init == x = TRUE 19 | Next == x' \in BOOLEAN 20 | Spec == Init /\ [][Next]_x 21 | ==== 22 | ``` 23 | 24 | In this example, one solution would be `Prop == []x`. Solutions to each exercise can be found in the `_solutions` folder. 25 | 26 | Additional rules: 27 | 28 | - You do not need to change anything about either spec *except* the definition of `Prop`. `Prop` will always be the same for both specs. 29 | - "Fail" means either an invariant is violated or temporal properties are violated. It does not count if you trigger a parse error, a deadlock, or a runtime exception (like comparing a string and an integer). 30 | - There may be multiple solutions to a given exercise. Try to find one that corresponds to what the spec is actually "doing", ie could be a realistic property in a realistic system. 31 | - Each exercise has a corresponding `cfg` file. All configs will check `PROPERTY Prop` but may also have additional setup, like disabling deadlocks. 32 | - If prop is an invariant, remember to prefix it with a `[]`. Write `Prop == []x`, not `Prop == x`. 33 | - As of now there are no refinements or action properties- everything is an invariant or liveness. 34 | 35 | Tips: 36 | 37 | - Try to determine how the two specs diverge: what's a behavior in the failing spec that isn't possible in the passing spec? 38 | - Specs prefixed with `wf_` or `sf_` cover weak/strong fairness. Do the weak fairness version of the exercise first! 39 | 40 | ### Purpose 41 | 42 | Writing properties is a core TLA+ skill. -------------------------------------------------------------------------------- /passfail/check_solutions.py: -------------------------------------------------------------------------------- 1 | """ 2 | A quick script that tests that all of the exercise solutions are correct (pass the _pass spec, fail the _fail spec). 3 | 4 | Written in part with Claude 3.5 5 | """ 6 | import subprocess 7 | import re 8 | from tempfile import TemporaryDirectory, NamedTemporaryFile # For state cleanup 9 | from pathlib import Path 10 | import argparse 11 | from dataclasses import dataclass 12 | 13 | @dataclass 14 | class Exercise: 15 | pass_: Path 16 | fail: Path 17 | solution: str 18 | cfg: Path 19 | name: str 20 | 21 | # TODO move process_tla into a method here 22 | 23 | def process_tla(source: Path, solution: str) -> str: 24 | content = source.read_text() 25 | 26 | content = re.sub( 27 | r'Prop\s*==\s*[^\n]+', 28 | solution.replace('\\', '\\\\'), # Escape the solution string to handle TLA+ syntax 29 | content 30 | ) 31 | 32 | # We want consistent naming of the files, but that means renaming the modules 33 | content = re.sub( 34 | r'---- MODULE .+ ----', 35 | r'---- MODULE Test ----', 36 | content 37 | ) 38 | 39 | return content 40 | 41 | def parse_result(result: str) -> str: 42 | if "unexpected exception" in result: 43 | return "E" 44 | elif "No error has been found." in result: 45 | return "+" 46 | elif "violated" in result: 47 | return "-" 48 | else: 49 | return "E" 50 | 51 | 52 | parser = argparse.ArgumentParser() 53 | parser.add_argument("--tools_path", 54 | default=r"D:/software/TLA+/tla2tools.jar", # Default to my computer's toolpath for now 55 | help="Path to tla2tools.jar") 56 | parser.add_argument("spec", nargs="?", help="Optional name of a specific spec to test") 57 | 58 | def get_exercise_folders(spec_name=None): 59 | root = Path(__file__).parent 60 | if spec_name: 61 | # If a specific spec is requested, look for the folder with that name 62 | folder = root / spec_name 63 | if folder.exists() and folder.is_dir(): 64 | return [folder] 65 | else: 66 | print(f"Warning: No exercise folder found with name '{spec_name}'") 67 | return [] 68 | else: 69 | # Otherwise return all folders 70 | return [f for f in root.iterdir() if f.is_dir() and f.name != '_solutions'] 71 | 72 | def run_tests(jar_path, spec_name=None): 73 | exercises = get_exercise_folders(spec_name) 74 | exercise_tests = [] 75 | 76 | for exercise in exercises: 77 | solution_file = Path(__file__).parent / '_solutions' / f'{exercise.name}.txt' 78 | if not solution_file.exists(): 79 | continue 80 | 81 | solution = Path(solution_file).read_text() 82 | 83 | # Get all files in exercise directory 84 | pass_files = list(exercise.glob('*_pass.tla'))[0] # temporary, assume one file for now 85 | fail_files = list(exercise.glob('*_fail.tla'))[0] # temporary, assume one file for now 86 | cfg = next(exercise.glob('*.cfg')) 87 | 88 | exercise_tests.append(Exercise( 89 | pass_=pass_files, 90 | fail=fail_files, 91 | solution=solution, 92 | name=exercise.name, 93 | cfg=cfg 94 | )) 95 | 96 | # Print table header before the test loop 97 | print(f"{'Exercise':<15} {'+ (Pass)':<15} {'- (Fail)':<15}") 98 | print("-" * 40) 99 | 100 | for test in exercise_tests: 101 | with TemporaryDirectory() as run_dir: 102 | 103 | # Copy config file 104 | mc_file = Path(run_dir) / 'Test.tla' 105 | mc_cfg = Path(run_dir) / 'Test.cfg' 106 | mc_cfg.write_text(test.cfg.read_text()) 107 | script = f"java -jar {jar_path} -workers auto -metadir {run_dir} -terse -cleanup {mc_file}" 108 | 109 | mc_file.write_text(process_tla(test.pass_, test.solution)) 110 | pass_result = subprocess.run(script, text=True, capture_output=True, shell=True) 111 | 112 | 113 | mc_file.write_text(process_tla(test.fail, test.solution)) 114 | fail_result = subprocess.run(script, text=True, capture_output=True, shell=True) 115 | 116 | print(f"{test.name:<15} {parse_result(pass_result.stdout):<15} {parse_result(fail_result.stdout):<15}") 117 | 118 | 119 | if __name__ == "__main__": 120 | args = parser.parse_args() 121 | run_tests(args.tools_path, args.spec) 122 | -------------------------------------------------------------------------------- /typeset/check_solutions.py: -------------------------------------------------------------------------------- 1 | """ 2 | A quick script that tests that all of the exercise solutions are correct 3 | 4 | Written in part with Claude 5 | """ 6 | import re 7 | from tempfile import TemporaryDirectory # For state cleanup 8 | from pathlib import Path 9 | import argparse 10 | import asyncio 11 | 12 | 13 | def process_tla(source: Path, solution: str) -> str: 14 | content = source.read_text() 15 | 16 | content = re.sub( 17 | r'Typeset.+\s*==\s*[^\n]+', 18 | solution.replace('\\', '\\\\'), # Escape the solution string to handle TLA+ syntax 19 | content 20 | ) 21 | 22 | # We want consistent naming of the files, but that means renaming the modules 23 | content = re.sub( 24 | r'---- MODULE .+ ----', 25 | r'---- MODULE Test ----', 26 | content 27 | ) 28 | 29 | return content 30 | 31 | def parse_result(result: str) -> str: 32 | if "unexpected exception" in result: 33 | return "E" 34 | elif "Correct" in result: 35 | return "+" 36 | elif "Incorrect" in result: 37 | return "-" 38 | else: 39 | return "E" 40 | 41 | async def run_tlc_process(script: str): 42 | proc = await asyncio.create_subprocess_shell( 43 | script, 44 | stdout=asyncio.subprocess.PIPE, 45 | stderr=asyncio.subprocess.PIPE, 46 | shell=True 47 | ) 48 | stdout, stderr = await proc.communicate() 49 | return stdout.decode() 50 | 51 | parser = argparse.ArgumentParser() 52 | parser.add_argument("--tools_path", 53 | default=r"D:/software/TLA+/tla2tools.jar", # Default to my computer's toolpath for now 54 | help="Path to tla2tools.jar") 55 | parser.add_argument("spec", nargs="?", help="Optional name of a specific spec to test") 56 | 57 | def get_exercise_files(spec_name=None) -> list[Path]: 58 | root = Path(__file__).parent 59 | if spec_name: 60 | # If a specific spec is requested, look for it 61 | files = list(root.glob(f"**/{spec_name}.tla")) 62 | if not files: 63 | print(f"Warning: No specification file found with name '{spec_name}'") 64 | return files 65 | else: 66 | # Otherwise return all TLA files 67 | return list(root.glob("**/*.tla")) 68 | 69 | async def run_test(exercise, jar_path, temp_dir_base, semaphore): 70 | # Use semaphore to limit concurrent execution 71 | async with semaphore: 72 | solution_file = Path(__file__).parent / '_solutions' / f'{exercise.stem}.txt' 73 | if not solution_file.exists(): 74 | return None 75 | 76 | solution = Path(solution_file).read_text() 77 | 78 | # Create a unique directory for each test 79 | run_dir = Path(temp_dir_base) / f"test_{exercise.stem}" 80 | run_dir.mkdir() 81 | 82 | mc_file = run_dir / 'Test.tla' 83 | mc_cfg = run_dir / 'Test.cfg' 84 | mc_cfg.write_text("SPECIFICATION Spec") 85 | script = f"java -jar {jar_path} -workers 1 -metadir {run_dir} -terse -cleanup {mc_file} -fpmem 20" 86 | 87 | mc_file.write_text(process_tla(exercise, solution)) 88 | result = await run_tlc_process(script) 89 | 90 | # Return a tuple with exercise name and result status 91 | return (exercise.stem, parse_result(result)) 92 | 93 | async def run_tests_async(jar_path, spec_name=None): 94 | exercises = get_exercise_files(spec_name) 95 | results = [] 96 | 97 | # Create a semaphore limiting to 5 concurrent tasks 98 | semaphore = asyncio.Semaphore(5) 99 | 100 | with TemporaryDirectory() as base_dir: 101 | # Create tasks for all exercises that have solution files 102 | tasks = [] 103 | for exercise in exercises: 104 | tasks.append(run_test(exercise, jar_path, base_dir, semaphore)) 105 | 106 | # Wait for all tasks to complete 107 | results = await asyncio.gather(*tasks) 108 | 109 | # Filter out None results (exercises without solutions) 110 | results = [r for r in results if r is not None] 111 | 112 | # Print results after all tests have finished 113 | print(f"{'Exercise':<15} {'+ (Pass)':<15}") 114 | print("-" * 30) 115 | for name, status in results: 116 | print(f"{name:<15} {status:<15}") 117 | 118 | if spec_name and not results: 119 | print(f"No solution found for '{spec_name}'") 120 | 121 | def run_tests(jar_path, spec_name=None): 122 | return asyncio.run(run_tests_async(jar_path, spec_name)) 123 | 124 | if __name__ == "__main__": 125 | args = parser.parse_args() 126 | run_tests(args.tools_path, args.spec) 127 | 128 | --------------------------------------------------------------------------------