Differences
This shows you the differences between two versions of the page.
| Both sides previous revision Previous revision | |||
| creating:safety [2025/08/29 17:44] – Fixed code highlighting ahelwer | creating:safety [2025/09/02 19:25] (current) – Finished first draft with challenges ahelwer | ||
|---|---|---|---|
| Line 27: | Line 27: | ||
| Our overall approach to checking safety properties has the following structure: | Our overall approach to checking safety properties has the following structure: | ||
| - | - Parse a TLA⁺ specification, | + | - Parse a TLA⁺ specification, |
| - Find the spec's initial state(s) satisfying '' | - Find the spec's initial state(s) satisfying '' | ||
| - Repeatedly use '' | - Repeatedly use '' | ||
| - | - Check to ensure the '' | + | - Check to ensure the invariants hold in every discovered state. |
| - | - If a state violates | + | - If a state violates |
| For this purpose, create a new file '' | For this purpose, create a new file '' | ||
| Line 56: | Line 56: | ||
| The real TLA⁺ tools allow users to identify the init, next and invariant predicates in a model configuration file. | The real TLA⁺ tools allow users to identify the init, next and invariant predicates in a model configuration file. | ||
| - | We skip all that and simply require our specs to have operator definitions named '' | + | We skip all that and simply require our specs to have operator definitions named '' |
| Iterate through all the spec statements to find them: | Iterate through all the spec statements to find them: | ||
| - | <code java [highlight_lines_extra=" | + | <code java [highlight_lines_extra=" |
| private final Interpreter interpreter; | private final Interpreter interpreter; | ||
| private Stmt.OpDef init = null; | private Stmt.OpDef init = null; | ||
| private Stmt.OpDef next = null; | private Stmt.OpDef next = null; | ||
| - | private Stmt.OpDef | + | private |
| ModelChecker(Interpreter interpreter, | ModelChecker(Interpreter interpreter, | ||
| Line 71: | Line 71: | ||
| Stmt.OpDef op = (Stmt.OpDef)unit; | Stmt.OpDef op = (Stmt.OpDef)unit; | ||
| switch (op.name.lexeme) { | switch (op.name.lexeme) { | ||
| - | case " | + | case " |
| - | case " | + | case " |
| - | case " | + | case " |
| + | case " | ||
| + | case " | ||
| } | } | ||
| } | } | ||
| Line 97: | Line 99: | ||
| Then add calls to '' | Then add calls to '' | ||
| <code java [highlight_lines_extra=" | <code java [highlight_lines_extra=" | ||
| - | validate(this.init, " | + | validate(init, |
| - | validate(this.next, " | + | validate(next, |
| - | | + | |
| } | } | ||
| </ | </ | ||
| + | As written it is possible for users to define no invariants at all, which is fine. | ||
| Step one complete! | Step one complete! | ||
| Line 110: | Line 112: | ||
| Create a new '' | Create a new '' | ||
| <code java> | <code java> | ||
| - | | + | |
| return null; | return null; | ||
| Line 116: | Line 118: | ||
| </ | </ | ||
| - | This method indicates success (the invariant | + | This method indicates success (the invariants |
| - | If the invariant does not hold in every state, the method returns a state trace leading to the failing state. | + | If the invariants do not hold in every state, the method returns a state trace leading to the failing state. |
| + | Define the '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | class ModelChecker { | ||
| + | |||
| + | record Step(String action, Map< | ||
| + | record StateTrace(String failingInvariant, | ||
| + | |||
| + | private final Interpreter interpreter; | ||
| + | </ | ||
| Let's talk about //how// we are going to explore the state space. | Let's talk about //how// we are going to explore the state space. | ||
| Line 141: | Line 152: | ||
| With all that out of the way, here's how we write the preamble to the main BFS loop in '' | With all that out of the way, here's how we write the preamble to the main BFS loop in '' | ||
| <code java [highlight_lines_extra=" | <code java [highlight_lines_extra=" | ||
| - | | + | |
| Deque< | Deque< | ||
| Map< | Map< | ||
| Line 176: | Line 187: | ||
| But to what end? | But to what end? | ||
| - | ===== Checking the Invariant | + | ===== Checking the Invariants |
| - | Here we ensure the invariant holds in every discovered state. | + | Here we ensure the invariants hold in every discovered state. |
| - | There is a minor design decision to be made: do we check the invariant | + | There is a minor design decision to be made: do we check the invariants |
| The latter turns out to be simpler because no special logic is necessary for the initial states, so that is what we do; add the highlighted code to '' | The latter turns out to be simpler because no special logic is necessary for the initial states, so that is what we do; add the highlighted code to '' | ||
| - | <code java [highlight_lines_extra=" | + | <code java [highlight_lines_extra=" |
| - | TlaCallable invariant = (TlaCallable)interpreter.globals.get(invariantDef.name); | + | |
| while (!pendingStates.isEmpty()) { | while (!pendingStates.isEmpty()) { | ||
| Map< | Map< | ||
| interpreter.goToState(current); | interpreter.goToState(current); | ||
| - | if (!(boolean)invariant.call(interpreter, new ArrayList<> | + | |
| - | return reconstructStateTrace(predecessors, | + | |
| + | return reconstructStateTrace(predecessors, | ||
| + | } | ||
| } | } | ||
| </ | </ | ||
| - | If the invariant fails then we return a state trace! | + | If an invariant fails then we return a state trace! |
| How do we construct that trace? | How do we construct that trace? | ||
| - | Here is how to implement the '' | + | Here is how to implement the '' |
| <code java> | <code java> | ||
| List< | List< | ||
| Map< | Map< | ||
| - | Map< | + | Map< |
| + | Stmt.OpDef invariant | ||
| ) { | ) { | ||
| List< | List< | ||
| Line 205: | Line 218: | ||
| predecessor = predecessors.get(predecessor); | predecessor = predecessors.get(predecessor); | ||
| } while (predecessor != null); | } while (predecessor != null); | ||
| - | | + | |
| + | |||
| + | } | ||
| + | </ | ||
| + | |||
| + | Then, convert the list of states into a '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | trace = trace.reversed(); | ||
| + | |||
| + | List< | ||
| + | Stmt.OpDef action = init; | ||
| + | for (Map< | ||
| + | steps.add(new Step(action.name.lexeme, | ||
| + | action = next; | ||
| + | } | ||
| + | |||
| + | return new StateTrace(invariant.name.lexeme, | ||
| } | } | ||
| </ | </ | ||
| Done! | Done! | ||
| - | We have successfully implemented a TLA⁺ model checker for safety properties! | + | We have successfully implemented a TLA⁺ model-checker for safety properties! |
| - | All that remains is to wire it up to the front-end. | + | |
| + | ===== Hooking Up the Front-End ===== | ||
| + | |||
| + | Now we modify '' | ||
| + | In past chapters we've focused on running TLA⁺ in a REPL where users can evaluate expressions and transition between states. | ||
| + | Full TLA⁺ source files cannot be run as programs; all we can do is model-check them. | ||
| + | |||
| + | The simplest place to call the model-checker is at the bottom of the '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | if (!replMode) { | ||
| + | ModelChecker mc = new ModelChecker(interpreter, | ||
| + | ModelChecker.StateTrace trace = mc.checkSafety(); | ||
| + | System.out.println( | ||
| + | trace == null | ||
| + | ? " | ||
| + | : " | ||
| + | ); | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Now we can model-check TLA⁺ specs! | ||
| + | What's a good first example? | ||
| + | The traditional "hello world" model-checking problem is the water jugs puzzle from the movie Die Hard 3, which you can watch [[https:// | ||
| + | I rewrote the [[https:// | ||
| + | |||
| + | <code haskell> | ||
| + | Jug == 1 .. 2 | ||
| + | Capacity == [ | ||
| + | j \in Jug |-> | ||
| + | IF j = 1 THEN 3 | ||
| + | ELSE IF j = 2 THEN 5 | ||
| + | ELSE 0 | ||
| + | ] | ||
| + | Goal == 4 | ||
| + | |||
| + | VARIABLE contents | ||
| + | |||
| + | Min(m, n) == IF m < n THEN m ELSE n | ||
| + | |||
| + | FillJug(target) == | ||
| + | contents' | ||
| + | j \in Jug |-> | ||
| + | IF j = target THEN Capacity[j] ELSE contents[j] | ||
| + | ] | ||
| + | |||
| + | EmptyJug(target) == | ||
| + | contents' | ||
| + | j \in Jug |-> | ||
| + | IF j = target THEN 0 ELSE contents[j] | ||
| + | ] | ||
| + | |||
| + | JugToJug(from, | ||
| + | /\ ~(from = to) | ||
| + | /\ contents' | ||
| + | j \in Jug |-> | ||
| + | IF j = from THEN | ||
| + | contents[j] - Min(contents[from], | ||
| + | ELSE IF j = to THEN | ||
| + | contents[j] + Min(contents[from], | ||
| + | ELSE | ||
| + | contents[j] | ||
| + | ] | ||
| + | |||
| + | TypeOK == \A j \in Jug : contents[j] \in 0 .. Capacity[j] | ||
| + | |||
| + | Init == contents = [j \in Jug |-> 0] | ||
| + | |||
| + | Next == | ||
| + | \E j \in Jug : | ||
| + | \/ FillJug(j) | ||
| + | \/ EmptyJug(j) | ||
| + | \/ \E k \in Jug : JugToJug(j, k) | ||
| + | |||
| + | Inv == \A j \in Jug : ~(contents[j] = Goal) | ||
| + | </ | ||
| + | |||
| + | You can vary the problem definition by modifying the definition of '' | ||
| + | As-is, it checks the classic Die Hard jugs puzzle with one 3-gallon jug and one 5-gallon jug. | ||
| + | We set the invariant to assert | ||
| + | When run on this spec, our model-checker spits out a state trace showing how to get 4 gallons of water in the 5-gallon jug! | ||
| + | |||
| + | After all the preliminary work, this chapter was surprisingly short. | ||
| + | We have now implemented the core of a minimal TLA⁺ model-checker for safety properties. | ||
| + | One chapter | ||
| + | It's important to understand, although somewhat optional to implement. | ||
| + | Read it [[creating: | ||
| + | |||
| + | ====== Challenges ====== | ||
| + | |||
| + | Here are some optional challenges to flesh out your TLA⁺ model-checker, | ||
| + | You should save a copy of your code before attempting these. | ||
| + | - The state trace presentation leaves something to be desired; override the '' | ||
| + | - To save on memory, real finite-state model-checkers like TLC don't store the entire state in the '' | ||
| + | - Ignoring the safety issues with using Java's built-in '' | ||
| + | - Currently, our code only specifies '' | ||
| + | |||
| + | [[creating: | ||