Differences
This shows you the differences between two versions of the page.
| Next revision | Previous revision | ||
| creating:safety [2025/08/27 18:46] – Created page with intro and first section ahelwer | creating:safety [2025/11/03 23:16] (current) – Removed extra line of code ahelwer | ||
|---|---|---|---|
| Line 13: | Line 13: | ||
| Safety properties are often informally described as " | Safety properties are often informally described as " | ||
| For example, when developing a multi-core CPU memory caching system, a good safety property would be that no two CPU caches ever disagree about the value stored at a given memory address. | For example, when developing a multi-core CPU memory caching system, a good safety property would be that no two CPU caches ever disagree about the value stored at a given memory address. | ||
| - | However, our systems often surprise us and find their ways to corners of the state space where a safety property does not hold. | + | However, our systems often surprise us and find their way to corners of the state space where a safety property does not hold. |
| When that happens, the best way to debug the problem is to see a path from some initial state to the state violating the invariant. | When that happens, the best way to debug the problem is to see a path from some initial state to the state violating the invariant. | ||
| This path is what is meant by a state trace counterexample of finite length. | This path is what is meant by a state trace counterexample of finite length. | ||
| 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, | ||
| this.interpreter = interpreter; | this.interpreter = interpreter; | ||
| for (Stmt unit : spec) { | for (Stmt unit : spec) { | ||
| - | if (unit instanceof Stmt.OpDef) { | + | if (unit instanceof Stmt.OpDef |
| - | 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 98: | ||
| 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! | ||
| - | ===== Finding | + | ===== Exploring |
| - | Now we kick off the main event. | + | Now for the main event - everything has led up to this! |
| - | Define | + | Create |
| <code java> | <code java> | ||
| - | | + | |
| return null; | return null; | ||
| Line 116: | Line 117: | ||
| </ | </ | ||
| - | This method indicates success (the invariant held in every state) by returning '' | + | This method indicates success (the invariants holds in every state) by returning '' |
| - | 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. | ||
| + | There are many, many methods of doing this. | ||
| + | We'll use [[https:// | ||
| + | BFS has the nice property that whenever we first encounter a state, we have also found the shortest path to that state from the initial state(s). | ||
| + | It works as follows: | ||
| + | - Push all initial states onto a queue of pending states, and add them to a set of visited states. | ||
| + | - While the pending state queue is not empty, pop the head state from the queue for examination. | ||
| + | - Find all next states reachable in one step from the state under examination. | ||
| + | - For each next state, if it has not already been visited then push it onto the pending state queue and add it to the set of visited states. | ||
| + | |||
| + | Not too bad! | ||
| + | Only two datastructures, | ||
| + | Assuming there are a finite number of states, BFS will eventually examine all states then terminate. | ||
| + | If there are an infinite number of states - an easy-to-accomplish feat in TLA⁺ - then BFS will never terminate, but that is a user problem. | ||
| + | |||
| + | There is one more thing we need to keep track of so we can reconstruct state traces: each state' | ||
| + | This combines nicely with the visited set; instead of a set of states, we use a map from states to their predecessors. | ||
| + | If a state is in the predecessor map, it must have been visited. | ||
| + | The predecessor of all initial states is '' | ||
| + | |||
| + | 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=" | ||
| + | StateTrace checkSafety() { | ||
| + | Deque< | ||
| + | Map< | ||
| + | for (Map< | ||
| + | predecessors.put(initialState, | ||
| + | pendingStates.add(initialState); | ||
| + | } | ||
| + | |||
| + | while (!pendingStates.isEmpty()) { | ||
| + | |||
| + | } | ||
| + | |||
| + | return null; | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Now all the initial states have been pushed onto the pending states queue and marked visited. | ||
| + | Here is the rest of the BFS algorithm, a loop that runs in '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | while (!pendingStates.isEmpty()) { | ||
| + | Map< | ||
| + | interpreter.goToState(current); | ||
| + | |||
| + | for (Map< | ||
| + | if (!predecessors.containsKey(next)) { | ||
| + | predecessors.put(next, | ||
| + | pendingStates.add(next); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | This will correctly explore the state space in breadth-first order. | ||
| + | But to what end? | ||
| + | |||
| + | ===== Checking the Invariants ===== | ||
| + | |||
| + | Here we ensure the invariants hold in every discovered state. | ||
| + | There is a minor design decision to be made: do we check the invariants upon first encountering a state, or later after popping it from the pending state queue? | ||
| + | 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=" | ||
| + | while (!pendingStates.isEmpty()) { | ||
| + | Map< | ||
| + | interpreter.goToState(current); | ||
| + | for (Stmt.OpDef invariant : invariants) { | ||
| + | if (!(boolean)invariant.body.accept(interpreter)) { | ||
| + | return reconstructStateTrace(predecessors, | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | If an invariant fails then we return a state trace! | ||
| + | How do we construct that trace? | ||
| + | Here is how to implement the '' | ||
| + | <code java> | ||
| + | List< | ||
| + | Map< | ||
| + | Map< | ||
| + | Stmt.OpDef invariant | ||
| + | ) { | ||
| + | List< | ||
| + | Map< | ||
| + | do { | ||
| + | trace.add(predecessor); | ||
| + | predecessor = predecessors.get(predecessor); | ||
| + | } while (predecessor != null); | ||
| + | trace = trace.reversed(); | ||
| + | |||
| + | } | ||
| + | </ | ||
| + | |||
| + | 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! | ||
| + | We have successfully implemented a TLA⁺ model-checker for safety properties! | ||
| + | |||
| + | ===== 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 that no jug will every contain 4 gallons of water. | ||
| + | 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 remains, clarifying how operator parameters interact with the prime operator. | ||
| + | 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: | ||