├── .gitignore ├── Animation.tla ├── README.md ├── examples ├── Elevator │ ├── Elevator.tla │ ├── ElevatorAnimated.tla │ └── elevator.html ├── LogVisibility │ ├── LogVisibility.tla │ ├── LogVisibilityAnimated.tla │ └── log_visibility.html ├── Raft │ ├── RaftAnimated.tla │ ├── RaftSimpler.tla │ └── raft_majority_rollback.html └── template.html └── new_examples └── Elevator ├── .gitignore ├── Elevator.tla ├── ElevatorAnimated.tla ├── MCElevatorAnimated.cfg ├── MCElevatorAnimated.tla ├── README.md ├── aliases.sh ├── get_tlatools.sh ├── model_check.sh ├── template.html ├── traceExp.txt └── trace_explore.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.toolbox 2 | *.pdf 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /Animation.tla: -------------------------------------------------------------------------------- 1 | -------------------------------------------- MODULE Animation -------------------------------------------- 2 | EXTENDS Naturals, Sequences, Integers, TLC, FiniteSets 3 | 4 | (**************************************************************************************************) 5 | (* The Animation module provides functionality for creating an interactive visual animation of a *) 6 | (* TLA+ specification. It allows you to visualize a particular execution trace by producing an *) 7 | (* SVG visualization for each "frame" i.e. state of the trace. This is done by defining a state *) 8 | (* expression called a "View", which produces a set of graphical elements based on the variables *) 9 | (* of a specification. For a specification with existing 'Init' and 'Next' predicates, an *) 10 | (* animation is defined as shown below: *) 11 | (* *) 12 | (* EXTENDS Animation *) 13 | (* *) 14 | (* View == \* User-defined state expression *) 15 | (* *) 16 | (* AnimSpec == *) 17 | (* /\ AnimatedInit(Init, View) *) 18 | (* /\ [][AnimatedNext(Next, View)]_<> *) 19 | (* *) 20 | (* where 'View' is a user defined state expression. 'vars' must be the tuple of all variables in *) 21 | (* your existing spec. The expressions AnimatedInit(Init, View) and AnimatedNext(Next, View) *) 22 | (* produce initial state and next state predicates that add auxiliary variables for tracking *) 23 | (* animation related state. These variables should not affect the existing spec, as long as *) 24 | (* there are no name conflicts. Adding these auxiliary variables may slow down model checking *) 25 | (* considerably. Often, simulation mode seems to be more efficient for generating animated *) 26 | (* execution traces, since it does not incur the memory overhead of maintaining an explicit queue *) 27 | (* of next states. Hopefully this slowdown is acceptable, since the intended purpose of this *) 28 | (* Animation module is less about improving verification of TLA+ specs, and more about providing *) 29 | (* an alternative way to communicate TLA+ specs and associated models. *) 30 | (* *) 31 | (**************************************************************************************************) 32 | 33 | 34 | (**************************************************************************************************) 35 | (* Helper Operators *) 36 | (**************************************************************************************************) 37 | 38 | \* Pick an arbitrary element of a given set 39 | Pick(S) == CHOOSE x \in S : TRUE 40 | 41 | \* Merge two records 42 | Merge(r1, r2) == 43 | LET D1 == DOMAIN r1 D2 == DOMAIN r2 IN 44 | [k \in (D1 \cup D2) |-> IF k \in D1 THEN r1[k] ELSE r2[k]] 45 | 46 | \* The set of all permutations of elements of a set S whose length are the cardinality of S. 47 | SeqPermutations(S) == LET Dom == 1..Cardinality(S) IN 48 | {f \in [Dom -> S] : \A w \in S : \E v \in Dom : f[v]=w} 49 | 50 | \* Convert a set to a sequence where the elements are in arbitrary order. 51 | RECURSIVE SetToSeq(_) 52 | SetToSeq(S) == IF S = {} THEN <<>> 53 | ELSE LET v == Pick(S) IN <> \o SetToSeq(S \ {v}) 54 | 55 | \* Join a sequence of strings with a specified delimiter. 56 | RECURSIVE Join(_, _) 57 | Join(seq, delim) == 58 | IF Len(seq) = 0 THEN "" 59 | ELSE IF Len(seq) = 1 THEN Head(seq) 60 | ELSE (Head(seq) \o delim \o Join(Tail(seq), delim)) 61 | 62 | \* Quotify a given string. 63 | Quote(s) == "'" \o s \o "'" 64 | 65 | ------------------------------------------ 66 | 67 | (**************************************************************************************************) 68 | (* *) 69 | (* Core Graphic Elements *) 70 | (* *) 71 | (* Graphic primitives are represented using the same structure as SVG elements, but it is not *) 72 | (* necessary for users of this module to understand that internal detail. These graphic *) 73 | (* primitives are what should be used to construct a 'View' expression. Elements can be *) 74 | (* organized hierarchically using the 'Group' element. *) 75 | (* *) 76 | (**************************************************************************************************) 77 | 78 | \* A local, less verbose operator for converting a value to a string. 79 | LOCAL _str(s) == ToString(s) 80 | 81 | 82 | \* SVG element constructor. 83 | LOCAL SVGElem(_name, _attrs, _children) == [name |-> _name, attrs |-> _attrs, children |-> _children ] 84 | 85 | \* Construct an SVG View element. 86 | LOCAL SVGView(w, h, children) == SVGElem("svg", [width |-> w, height |-> h], children) 87 | 88 | \* Special element that SVGElemToString will interpret as a raw string. 89 | LOCAL RawText(text) == SVGElem("_rawtext", [val |-> text], <<>>) 90 | 91 | \* Convert an SVG element into its string representation. 92 | RECURSIVE SVGElemToString(_) 93 | SVGElemToString(elem) == 94 | IF elem.name = "_rawtext" THEN elem.attrs.val ELSE 95 | LET childrenStr == Join([i \in DOMAIN elem.children |-> SVGElemToString(elem.children[i])], "") IN 96 | LET attrsStrSet == {Join(<>, "=") : k \in DOMAIN elem.attrs} IN 97 | LET attrsStr == Join(SetToSeq(attrsStrSet), " ") IN 98 | Join(<<"<", elem.name, " ", attrsStr, ">", childrenStr , "">>, "") 99 | 100 | \* All elements below can accept an 'attrs' argument. This is a function mapping string 101 | \* keys to string values. These key-value pairs describe any additional SVG attributes of the element. To pass 102 | \* no attributes, just pass attrs=<<>>. 103 | 104 | \* Line element. 'x1', 'y1', 'x2', and 'y2' should be given as integers. 105 | Line(x1, y1, x2, y2, attrs) == 106 | LET svgAttrs == [x1 |-> _str(x1), 107 | y1 |-> _str(y1), 108 | x2 |-> _str(x2), 109 | y2 |-> _str(y2)] IN 110 | SVGElem("line", Merge(svgAttrs, attrs), <<>>) 111 | 112 | \* Circle element. 'cx', 'cy', and 'r' should be given as integers. 113 | Circle(cx, cy, r, attrs) == 114 | LET svgAttrs == [cx |-> _str(cx), 115 | cy |-> _str(cy), 116 | r |-> _str(r)] IN 117 | SVGElem("circle", Merge(svgAttrs, attrs), <<>>) 118 | 119 | \* Rectangle element. 'x', 'y', 'w', and 'h' should be given as integers. 120 | Rect(x, y, w, h, attrs) == 121 | LET svgAttrs == [x |-> _str(x), 122 | y |-> _str(y), 123 | width |-> _str(w), 124 | height |-> _str(h)] IN 125 | SVGElem("rect", Merge(svgAttrs, attrs), <<>>) 126 | 127 | \* Text element.'x', and 'y' should be given as integers, and 'text' given as a string. 128 | Text(x, y, text, attrs) == 129 | LET svgAttrs == [x |-> _str(x), 130 | y |-> _str(y)] IN 131 | SVGElem("text", Merge(svgAttrs, attrs), <>) 132 | 133 | \* Group element. 'children' is as a sequence of elements that will be contained in this group. 134 | Group(children, attrs) == SVGElem("g", attrs, children) 135 | 136 | ------------------------------------------ 137 | 138 | (**************************************************************************************************) 139 | (* *) 140 | (* Animation Operators and Variables *) 141 | (* *) 142 | (* The variables below are used to construct a sequence of animation frames. Upon each step of *) 143 | (* an execution trace, we construct a new frame and convert it to an SVG string, and append it to *) 144 | (* the global 'svgAnimationString' variable. When the trace completes, this string should be *) 145 | (* suitable to copy into an HTML template that displays an animation frame sequence. *) 146 | (* *) 147 | (**************************************************************************************************) 148 | 149 | \* The global SVG string that stores the sequence of all animation frames. 150 | VARIABLE svgAnimationString 151 | 152 | \* Index representing what frame number we are currently on. 153 | VARIABLE frameInd 154 | 155 | \* The name of the current action being executed. (Optional) 156 | VARIABLE actionName 157 | 158 | AnimationVars == <> 159 | 160 | LOCAL ActionNameElem(name) == Text(10, 30, "Next Action: " \o name, <<>>) 161 | 162 | \* Builds a single frame 'i' for part of a sequence of animation frames. This is an SVG group element that 163 | \* contains identifying information about the frame. 164 | LOCAL MakeFrame(elem, action, i) == 165 | LET attrs == [class |-> "frame", id |-> ToString(i), action |-> action] IN 166 | Group(<>, attrs) 167 | 168 | ActionName(str) == actionName' = str 169 | 170 | \* Produces an initial state predicate for animation. *) 171 | AnimatedInit(Init, View) == 172 | /\ Init 173 | /\ frameInd = 0 174 | /\ actionName = "" 175 | /\ svgAnimationString = SVGElemToString(MakeFrame(View, "Init", 0)) 176 | 177 | \* 178 | \* Produces a next-state relation for animation. 179 | \* 180 | \* 'View' is a state expression that produces a graphic element visualizing the state of a spec's 181 | \* variables. 'Next' is the next state relation of the original spec. 182 | \* 183 | AnimatedNext(Next, View, UseActionNames) == 184 | /\ Next 185 | /\ frameInd' = frameInd + 1 186 | /\ IF UseActionNames THEN TRUE ELSE UNCHANGED actionName 187 | \* For efficiency, we don't explicitly keep a running sequence of all animation 188 | \* frames. When an action occurs, we simply generate the current frame, convert it 189 | \* to its SVG string representation, and append the string to the existing, global 190 | \* SVG animation string. This way we don't have to regenerate the SVG strings 191 | \* for past frames every time a new action occurs. 192 | /\ LET frame == MakeFrame(View, actionName', frameInd') IN 193 | svgAnimationString' = svgAnimationString \o SVGElemToString(frame) 194 | 195 | 196 | ==================================================================================================== 197 | \* Modification History 198 | \* Last modified Sun Jul 08 21:00:11 EDT 2018 by williamschultz 199 | \* Created Thu Mar 22 23:59:48 EDT 2018 by williamschultz 200 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TLA+ Animation Module 2 | 3 | This is a TLA+ module for creating visualizations of TLC execution traces that can be run inside a web browser. To create an animation, you can start with an existing TLA+ specification and define a "view" expression, which is a TLA+ state expression that produces a set of graphical elements based on the values of the variables declared in your specification. The Animation module, when used in conjunction with TLC, constructs a sequence of frames, where each frame is the value of the view expression at a step in an execution trace. The module uses SVG elements as its graphical primitives, which allows for flexibility and variety in the visualizations that can be produced. 4 | 5 | ### Update: December 2024 6 | 7 | These visualization features are now integrated into the web-based interface for exploring TLA+ specs found [here](https://github.com/will62794/tla-web), so this repository can be considered as somewhat obsolete. You can see an example of an animated spec in the web explorer [here](https://will62794.github.io/tla-web/#!/home?specpath=.%2Fspecs%2FCabbageGoatWolf.tla). 8 | 9 | ### Update: February 2020 10 | 11 | The original version of this module, described below and explained in more detail [here](https://www.youtube.com/watch?v=mLF220fPrP4&t=2s), is now somewhat obsolete. TLC [now supports](https://github.com/tlaplus/tlaplus/issues/393) running trace exploration from the command line, which makes it simpler to produce TLA+ animations. The original `Animation.tla` module has been revised a bit and pushed to the CommunityModules repo as a `SVG.tla` [module](https://github.com/tlaplus/CommunityModules/blob/4a1032a541837e4775d48e5efab56ce1f026edf8/modules/SVG.tla), which provides primitives for laying out visualizations. See the [new_examples/Elevator](new_examples/Elevator) directory for a demonstration of animating a trace with this new TLC functionality. It can be done entirely from the command line with no explicit need for TLA+ Toolbox. 12 | 13 | 14 | ## How To Use 15 | 16 | If you have an existing specification with initial state and next state predicates `Init` and `Next`, and with variables `vars`, you can define an animated version of this spec by defining the following expression: 17 | 18 | ```tla 19 | AnimatedSpec == 20 | /\ AnimatedInit(Init, View) 21 | /\ [][AnimatedNext(Next, View, FALSE)]_<> 22 | ``` 23 | 24 | where `View` is your defined view state expression. To produce the animation, you can set `AnimatedSpec` as the specification for TLC to check, and set up some way for TLC to produce a specific error trace. For example, you could define an invariant whose violation produces an interesting execution you would like to visualize. Or, you could just run TLC in simulation mode to see what some random execution looks like when visualized. When the error trace is produced, the state of the `svgAnimationString` variable for the final state will be a string containing a sequence of SVG elements where each is a frame of that trace. You can copy and paste this string into an HTML template and view the animation in a browser. 25 | 26 | If you are using the TLA+ Toolbox, a convenient way to define an animation for an existing spec called `Spec.tla` is to create a new spec called `SpecAnimated.tla`. Inside `SpecAnimated.tla` you can define your view expression and any related animation operators, and define the animated version of your spec (`AnimatedSpec` as shown in the example snippet above). In order to have access to the operators and variables of your original module, and also to those of the `Animation` module you can add the directory of `Spec.tla` and `Animation.tla` to the TLA+ library path locations. You can edit these by clicking on the name of a spec in the toolbox and selecting `Properties`. Doing this will then allow you to automatically include your original module and the `Animation` module, without explicitly copying them to your new animated spec's directory. Also, any changes you make to your original spec will automatically be reflected in the version included in your animated spec. 27 | 28 | ## Related Tools 29 | 30 | Many of the ideas behind the Animation module were inspired by the [ProB animator tool](https://www3.hhu.de/stups/prob/index.php/The_ProB_Animator_and_Model_Checker) and the [Runway specification language](https://engineering.salesforce.com/runway-intro-dc0d9578e248), both of which provide built-in visualization tools. One main goal of this module is to provide a visualization tool that is simple to use and comfortable for existing TLA+ users. This is enabled by the fact that animations can be described directly in TLA+, allowing them to take advantage of the expressiveness of the language. 31 | 32 | -------------------------------------------------------------------------------- /examples/Elevator/Elevator.tla: -------------------------------------------------------------------------------- 1 | ------------------------------------- MODULE Elevator ------------------------------------- 2 | (***************************************************************************) 3 | (* This spec describes a simple multi-car elevator system. The actions in *) 4 | (* this spec are unsurprising and common to all such systems except for *) 5 | (* DispatchElevator, which contains the logic to determine which elevator *) 6 | (* ought to service which call. The algorithm used is very simple and does *) 7 | (* not optimize for global throughput or average wait time. The *) 8 | (* TemporalInvariant definition ensures this specification provides *) 9 | (* capabilities expected of any elevator system, such as people eventually *) 10 | (* reaching their destination floor. *) 11 | (* *) 12 | (* Original spec: https://github.com/ahelwer/runway-tla-eval/Elevator.tla *) 13 | (***************************************************************************) 14 | 15 | EXTENDS Integers 16 | 17 | CONSTANTS Person, \* The set of all people using the elevator system 18 | Elevator, \* The set of all elevators 19 | FloorCount \* The number of floors serviced by the elevator system 20 | 21 | VARIABLES PersonState, \* The state of each person 22 | ActiveElevatorCalls, \* The set of all active elevator calls 23 | ElevatorState \* The state of each elevator 24 | 25 | Vars == \* Tuple of all specification variables 26 | <> 27 | 28 | Floor == \* The set of all floors 29 | 1 .. FloorCount 30 | 31 | Direction == \* Directions available to this elevator system 32 | {"Up", "Down"} 33 | 34 | ElevatorCall == \* The set of all elevator calls 35 | [floor : Floor, direction : Direction] 36 | 37 | ElevatorDirectionState == \* Elevator movement state; it is either moving in a direction or stationary 38 | Direction \cup {"Stationary"} 39 | 40 | GetDistance[f1, f2 \in Floor] == \* The distance between two floors 41 | IF f1 > f2 THEN f1 - f2 ELSE f2 - f1 42 | 43 | GetDirection[current, destination \in Floor] == \* Direction of travel required to move between current and destination floors 44 | IF destination > current THEN "Up" ELSE "Down" 45 | 46 | CanServiceCall[e \in Elevator, c \in ElevatorCall] == \* Whether elevator is in position to immediately service call 47 | LET eState == ElevatorState[e] IN 48 | /\ c.floor = eState.floor 49 | /\ c.direction = eState.direction 50 | 51 | PeopleWaiting[f \in Floor, d \in Direction] == \* The set of all people waiting on an elevator call 52 | {p \in Person : 53 | /\ PersonState[p].location = f 54 | /\ PersonState[p].waiting 55 | /\ GetDirection[PersonState[p].location, PersonState[p].destination] = d} 56 | 57 | TypeInvariant == \* Statements about the variables which we expect to hold in every system state 58 | /\ PersonState \in [Person -> [location : Floor \cup Elevator, destination : Floor, waiting : BOOLEAN]] 59 | /\ ActiveElevatorCalls \subseteq ElevatorCall 60 | /\ ElevatorState \in [Elevator -> [floor : Floor, direction : ElevatorDirectionState, doorsOpen : BOOLEAN, buttonsPressed : SUBSET Floor]] 61 | 62 | SafetyInvariant == \* Some more comprehensive checks beyond the type invariant 63 | /\ \A e \in Elevator : \* An elevator has a floor button pressed only if a person in that elevator is going to that floor 64 | /\ \A f \in ElevatorState[e].buttonsPressed : 65 | /\ \E p \in Person : 66 | /\ PersonState[p].location = e 67 | /\ PersonState[p].destination = f 68 | /\ \A p \in Person : \* A person is in an elevator only if the elevator is moving toward their destination floor 69 | /\ \A e \in Elevator : 70 | /\ (PersonState[p].location = e /\ ElevatorState[e].floor /= PersonState[p].destination) => 71 | /\ ElevatorState[e].direction = GetDirection[ElevatorState[e].floor, PersonState[p].destination] 72 | /\ \A c \in ActiveElevatorCalls : PeopleWaiting[c.floor, c.direction] /= {} \* No ghost calls 73 | 74 | TemporalInvariant == \* Expectations about elevator system capabilities 75 | /\ \A c \in ElevatorCall : \* Every call is eventually serviced by an elevator 76 | /\ c \in ActiveElevatorCalls ~> \E e \in Elevator : CanServiceCall[e, c] 77 | /\ \A p \in Person : \* If a person waits for their elevator, they'll eventually arrive at their floor 78 | /\ PersonState[p].waiting ~> PersonState[p].location = PersonState[p].destination 79 | 80 | PickNewDestination(p) == \* Person decides they need to go to a different floor 81 | LET pState == PersonState[p] IN 82 | /\ ~pState.waiting 83 | /\ pState.location \in Floor 84 | /\ \E f \in Floor : 85 | /\ f /= pState.location 86 | /\ PersonState' = [PersonState EXCEPT ![p] = [@ EXCEPT !.destination = f]] 87 | /\ UNCHANGED <> 88 | 89 | CallElevator(p) == \* Person calls the elevator to go in a certain direction from their floor 90 | LET pState == PersonState[p] IN 91 | LET call == [floor |-> pState.location, direction |-> GetDirection[pState.location, pState.destination]] IN 92 | /\ ~pState.waiting 93 | /\ pState.location /= pState.destination 94 | /\ ActiveElevatorCalls' = 95 | IF \E e \in Elevator : 96 | /\ CanServiceCall[e, call] 97 | /\ ElevatorState[e].doorsOpen 98 | THEN ActiveElevatorCalls 99 | ELSE ActiveElevatorCalls \cup {call} 100 | /\ PersonState' = [PersonState EXCEPT ![p] = [@ EXCEPT !.waiting = TRUE]] 101 | /\ UNCHANGED <> 102 | 103 | OpenElevatorDoors(e) == \* Open the elevator doors if there is a call on this floor or the button for this floor was pressed. 104 | LET eState == ElevatorState[e] IN 105 | /\ ~eState.doorsOpen 106 | /\ \/ \E call \in ActiveElevatorCalls : CanServiceCall[e, call] 107 | \/ eState.floor \in eState.buttonsPressed 108 | /\ ElevatorState' = [ElevatorState EXCEPT ![e] = [@ EXCEPT !.doorsOpen = TRUE, !.buttonsPressed = @ \ {eState.floor}]] 109 | /\ ActiveElevatorCalls' = ActiveElevatorCalls \ {[floor |-> eState.floor, direction |-> eState.direction]} 110 | /\ UNCHANGED <> 111 | 112 | EnterElevator(e) == \* All people on this floor who are waiting for the elevator and travelling the same direction enter the elevator. 113 | LET eState == ElevatorState[e] IN 114 | LET gettingOn == PeopleWaiting[eState.floor, eState.direction] IN 115 | LET destinations == {PersonState[p].destination : p \in gettingOn} IN 116 | /\ eState.doorsOpen 117 | /\ eState.direction /= "Stationary" 118 | /\ gettingOn /= {} 119 | /\ PersonState' = [p \in Person |-> 120 | IF p \in gettingOn 121 | THEN [PersonState[p] EXCEPT !.location = e] 122 | ELSE PersonState[p]] 123 | /\ ElevatorState' = [ElevatorState EXCEPT ![e] = [@ EXCEPT !.buttonsPressed = @ \cup destinations]] 124 | /\ UNCHANGED <> 125 | 126 | ExitElevator(e) == \* All people whose destination is this floor exit the elevator. 127 | LET eState == ElevatorState[e] IN 128 | LET gettingOff == {p \in Person : PersonState[p].location = e /\ PersonState[p].destination = eState.floor} IN 129 | /\ eState.doorsOpen 130 | /\ gettingOff /= {} 131 | /\ PersonState' = [p \in Person |-> 132 | IF p \in gettingOff 133 | THEN [PersonState[p] EXCEPT !.location = eState.floor, !.waiting = FALSE] 134 | ELSE PersonState[p]] 135 | /\ UNCHANGED <> 136 | 137 | CloseElevatorDoors(e) == \* Close the elevator doors once all people have entered and exited the elevator on this floor. 138 | LET eState == ElevatorState[e] IN 139 | /\ ~ENABLED EnterElevator(e) 140 | /\ ~ENABLED ExitElevator(e) 141 | /\ eState.doorsOpen 142 | /\ ElevatorState' = [ElevatorState EXCEPT ![e] = [@ EXCEPT !.doorsOpen = FALSE]] 143 | /\ UNCHANGED <> 144 | 145 | MoveElevator(e) == \* Move the elevator to the next floor unless we have to open the doors here. 146 | LET eState == ElevatorState[e] IN 147 | LET nextFloor == IF eState.direction = "Up" THEN eState.floor + 1 ELSE eState.floor - 1 IN 148 | /\ eState.direction /= "Stationary" 149 | /\ ~eState.doorsOpen 150 | /\ eState.floor \notin eState.buttonsPressed 151 | /\ \A call \in ActiveElevatorCalls : \* Can move only if other elevator servicing call 152 | /\ CanServiceCall[e, call] => 153 | /\ \E e2 \in Elevator : 154 | /\ e /= e2 155 | /\ CanServiceCall[e2, call] 156 | /\ nextFloor \in Floor 157 | /\ ElevatorState' = [ElevatorState EXCEPT ![e] = [@ EXCEPT !.floor = nextFloor]] 158 | /\ UNCHANGED <> 159 | 160 | StopElevator(e) == \* Stops the elevator if it's moved as far as it can in one direction 161 | LET eState == ElevatorState[e] IN 162 | LET nextFloor == IF eState.direction = "Up" THEN eState.floor + 1 ELSE eState.floor - 1 IN 163 | /\ ~ENABLED OpenElevatorDoors(e) 164 | /\ ~eState.doorsOpen 165 | /\ nextFloor \notin Floor 166 | /\ ElevatorState' = [ElevatorState EXCEPT ![e] = [@ EXCEPT !.direction = "Stationary"]] 167 | /\ UNCHANGED <> 168 | 169 | (***************************************************************************) 170 | (* This action chooses an elevator to service the call. The simple *) 171 | (* algorithm picks the closest elevator which is either stationary or *) 172 | (* already moving toward the call floor in the same direction as the call. *) 173 | (* The system keeps no record of assigning an elevator to service a call. *) 174 | (* It is possible no elevator is able to service a call, but we are *) 175 | (* guaranteed an elevator will eventually become available. *) 176 | (***************************************************************************) 177 | DispatchElevator(c) == 178 | LET stationary == {e \in Elevator : ElevatorState[e].direction = "Stationary"} IN 179 | LET approaching == {e \in Elevator : 180 | /\ ElevatorState[e].direction = c.direction 181 | /\ \/ ElevatorState[e].floor = c.floor 182 | \/ GetDirection[ElevatorState[e].floor, c.floor] = c.direction } IN 183 | /\ c \in ActiveElevatorCalls 184 | /\ stationary \cup approaching /= {} 185 | /\ ElevatorState' = 186 | LET closest == CHOOSE e \in stationary \cup approaching : 187 | /\ \A e2 \in stationary \cup approaching : 188 | /\ GetDistance[ElevatorState[e].floor, c.floor] <= GetDistance[ElevatorState[e2].floor, c.floor] IN 189 | IF closest \in stationary 190 | THEN [ElevatorState EXCEPT ![closest] = [@ EXCEPT !.floor = c.floor, !.direction = c.direction]] 191 | ELSE ElevatorState 192 | /\ UNCHANGED <> 193 | 194 | Init == 195 | \* Have people start at a random floor. 196 | /\ PersonState \in [Person -> [location : Floor, destination : Floor, waiting : {FALSE}]] 197 | /\ ActiveElevatorCalls = {} 198 | \* Have all elevators start at the first floor. 199 | /\ ElevatorState \in [Elevator -> [floor : {1}, direction : {"Stationary"}, doorsOpen : {FALSE}, buttonsPressed : {{}}]] 200 | 201 | Next == \* The next-state relation 202 | \/ \E p \in Person : PickNewDestination(p) 203 | \/ \E p \in Person : CallElevator(p) 204 | \/ \E e \in Elevator : OpenElevatorDoors(e) 205 | \/ \E e \in Elevator : EnterElevator(e) 206 | \/ \E e \in Elevator : ExitElevator(e) 207 | \/ \E e \in Elevator : CloseElevatorDoors(e) 208 | \/ \E e \in Elevator : MoveElevator(e) 209 | \/ \E e \in Elevator : StopElevator(e) 210 | \/ \E c \in ElevatorCall : DispatchElevator(c) 211 | 212 | TemporalAssumptions == \* Assumptions about how elevators and people will behave 213 | /\ \A p \in Person : WF_Vars(CallElevator(p)) 214 | /\ \A e \in Elevator : WF_Vars(OpenElevatorDoors(e)) 215 | /\ \A e \in Elevator : WF_Vars(EnterElevator(e)) 216 | /\ \A e \in Elevator : WF_Vars(ExitElevator(e)) 217 | /\ \A e \in Elevator : SF_Vars(CloseElevatorDoors(e)) 218 | /\ \A e \in Elevator : SF_Vars(MoveElevator(e)) 219 | /\ \A e \in Elevator : WF_Vars(StopElevator(e)) 220 | /\ \A c \in ElevatorCall : SF_Vars(DispatchElevator(c)) 221 | 222 | Spec == 223 | /\ Init 224 | /\ [][Next]_Vars 225 | \* /\ TemporalAssumptions 226 | 227 | THEOREM Spec => [](TypeInvariant /\ SafetyInvariant /\ TemporalInvariant) 228 | 229 | ==================================================================================================== 230 | \* Modification History 231 | \* Last modified Wed Mar 28 20:46:03 EDT 2018 by williamschultz 232 | \* Created Fri Mar 23 00:50:27 EDT 2018 by williamschultz 233 | 234 | \* Modification History 235 | \* Created Sun Mar 25 12:32:39 EDT 2018 by williamschultz 236 | -------------------------------------------------------------------------------- /examples/Elevator/ElevatorAnimated.tla: -------------------------------------------------------------------------------- 1 | ------------------------------------- MODULE ElevatorAnimated ------------------------------------- 2 | 3 | (**************************************************************************************************) 4 | (* Animation and View Definitions for Elevator system *) 5 | (**************************************************************************************************) 6 | 7 | EXTENDS Elevator, Animation 8 | 9 | 10 | (* View helpers. *) 11 | Injective(f) == \A x, y \in DOMAIN f : f[x] = f[y] => x = y 12 | 13 | \* Establish a fixed mapping to assign an ordering to elements in these sets. 14 | PersonId == CHOOSE f \in [Person -> 1..Cardinality(Person)] : Injective(f) 15 | ElevatorId == CHOOSE f \in [Elevator -> 1..Cardinality(Elevator)] : Injective(f) 16 | 17 | \* Dimensions of an elevator. 18 | ElevatorDims == [width |-> 35, height |-> 50] 19 | FloorHeight == ElevatorDims.height + 10 20 | FloorYPos(f) == f * FloorHeight 21 | 22 | \* Gives the (x,y) base position of an elevator. 23 | ElevatorPos(e) == [x |-> (150 + ElevatorId[e] * (ElevatorDims.width + 3)), y |-> FloorYPos(ElevatorState[e].floor)] 24 | 25 | (**************************************************************************************************) 26 | (* ELEVATOR elements. *) 27 | (**************************************************************************************************) 28 | ElevatorElem(e) == 29 | LET pos == ElevatorPos(e) 30 | dims == ElevatorDims 31 | color == IF ElevatorState[e].doorsOpen THEN "green" ELSE "black" IN 32 | Rect(ToString(pos.x), ToString(pos.y), ToString(dims.width), ToString(dims.height), [fill |-> color]) 33 | 34 | \* Elements that show which direction an elevator is moving. 35 | ElevatorDirElem(e) == 36 | LET pos == ElevatorPos(e) 37 | dims == ElevatorDims 38 | mid == pos.y + 17 39 | yPos == IF ElevatorState[e].direction = "Down" 40 | THEN mid - 17 41 | ELSE IF ElevatorState[e].direction = "Up" THEN mid + 17 42 | ELSE mid IN 43 | Rect(ToString(pos.x + 1), ToString(yPos), ToString(dims.width-2), "2", [fill |-> "white"]) 44 | 45 | ElevatorDirElems == {ElevatorDirElem(e) : e \in Elevator} 46 | ElevatorElems == {ElevatorElem(e) : e \in Elevator} 47 | 48 | (**************************************************************************************************) 49 | (* PERSON Elements. *) 50 | (**************************************************************************************************) 51 | 52 | PersonRadius == 3 53 | PersonXPosBase == 30 54 | PersonXPos(xBase, p) == xBase + (p * 9) 55 | PersonYPos(p) == FloorYPos(PersonState[p].location) + 10 56 | 57 | \* Person who is currently on a floor, not in an elevator. 58 | FloorPersonElem(p) == 59 | LET person == PersonState[p] 60 | pos == [y |-> PersonYPos(p), x |-> PersonXPos(PersonXPosBase, p)] 61 | color == IF person.waiting THEN "darkred" ELSE "blue" IN 62 | Circle(ToString(pos.x), ToString(pos.y), ToString(PersonRadius), [fill |-> color]) 63 | 64 | \* Person who is currently in an elevator. 65 | ElevatorPersonElem(p) == 66 | LET person == PersonState[p] 67 | elevPos == ElevatorPos(person.location) 68 | pos == [elevPos EXCEPT !.x = PersonXPos(elevPos.x, p), !.y = @ + 10] IN 69 | Circle(ToString(pos.x), ToString(pos.y), ToString(PersonRadius), [fill |-> "gray"]) 70 | 71 | PersonElem(p) == 72 | \* A person should always be waiting or in an elevator. 73 | LET person == PersonState[p] IN 74 | CASE person.location \in Floor -> FloorPersonElem(p) 75 | [] person.location \in Elevator -> ElevatorPersonElem(p) 76 | 77 | PersonDestinationElem(p) == 78 | LET person == PersonState[p] IN 79 | CASE person.location \in Floor -> 80 | LET xPos == PersonXPos(PersonXPosBase, p) 81 | dims == (IF (person.destination > person.location) 82 | THEN [height |-> (FloorYPos(person.destination) - PersonYPos(p)), 83 | yPos |-> PersonYPos(p)] 84 | ELSE [height |-> (PersonYPos(p) - FloorYPos(person.destination)), 85 | yPos |-> FloorYPos(person.destination)]) IN 86 | Rect(ToString(xPos), ToString(dims.yPos), "1", ToString(dims.height), [fill |-> "lightgray"]) 87 | [] person.location \in Elevator -> 88 | LET elevator == ElevatorState[person.location] 89 | elevPos == ElevatorPos(person.location) 90 | xPos == PersonXPos(elevPos.x, p) 91 | personYPos == elevPos.y + 10 92 | dims == (IF (person.destination > elevator.floor) 93 | THEN [height |-> (FloorYPos(person.destination) - personYPos), 94 | yPos |-> personYPos] 95 | ELSE [height |-> (personYPos - FloorYPos(person.destination)), 96 | yPos |-> FloorYPos(person.destination)]) IN 97 | Rect(ToString(xPos), ToString(dims.yPos), "1", ToString(dims.height), [fill |-> "lightgray"]) 98 | 99 | PersonDestinationElems == {PersonDestinationElem(p) : p \in Person} 100 | PeopleTitle == Text(ToString(PersonXPosBase), "30", "People", <<>>) 101 | PersonElems == {PersonElem(p) : p \in Person} \cup PersonDestinationElems 102 | 103 | (**************************************************************************************************) 104 | (* ELEVATOR CALL elements. *) 105 | (**************************************************************************************************) 106 | IsFloorCall(floor, dir) == \E c \in ActiveElevatorCalls : c.floor = floor /\ c.direction = dir 107 | 108 | ButtonXPos == 90 109 | Button(floor, dir) == 110 | LET x == ButtonXPos 111 | y == FloorYPos(floor) + (IF dir = "Up" THEN 25 ELSE 16) IN 112 | Rect(ToString(x), ToString(y), "7", "7", [fill |-> IF IsFloorCall(floor, dir) THEN "orange" ELSE "black"]) 113 | 114 | ElevatorButtonElem(floor) == 115 | LET upButton == Button(floor, "Up") 116 | downButton == Button(floor, "Down") IN 117 | Group(<>, <<>>) 118 | 119 | ElevatorButtonElems == {ElevatorButtonElem(f) : f \in Floor} 120 | 121 | (**************************************************************************************************) 122 | (* FLOOR elements. *) 123 | (**************************************************************************************************) 124 | 125 | FloorSeparator(floor) == Rect(ToString(5), ToString(FloorYPos(floor)), "350", "1", [fill |-> "lightgray"]) 126 | FloorSeparators == {FloorSeparator(f) : f \in Floor} 127 | 128 | FloorLabel(floor) == Text("10", ToString(FloorYPos(floor)+15), ToString(floor), <<>>) 129 | FloorLabels == {FloorLabel(f) : f \in Floor} 130 | 131 | FloorElems == UNION {FloorLabels, FloorSeparators} 132 | 133 | AllElems == SetToSeq(ElevatorElems) \o SetToSeq(UNION {FloorElems, ElevatorDirElems, ElevatorButtonElems, PersonElems}) 134 | 135 | View == Group(AllElems, ("transform" :> "translate(400 30)")) 136 | 137 | ----------------------------------------------------------------- 138 | Maximum(S) == CHOOSE x \in S : \A y \in S : x >= y 139 | 140 | ReducedInit == 141 | \* Have people start at a random even floor. Reduce the number of initial states by limiting initial destinations. 142 | /\ PersonState \in [Person -> [location : {x \in Floor : x % 2 = 0 }, 143 | destination : {1, Maximum(Floor)}, 144 | waiting : {FALSE}]] 145 | /\ ActiveElevatorCalls = {} 146 | \* Have all elevators start at the first floor. 147 | /\ ElevatorState \in [Elevator -> [floor : {1}, direction : {"Stationary"}, doorsOpen : {FALSE}, buttonsPressed : {{}}]] 148 | 149 | AnnotatedNext == \* The animation next-state relation. 150 | \/ \E p \in Person : PickNewDestination(p) /\ ActionName("PickNewDestination") 151 | \/ \E p \in Person : CallElevator(p) /\ ActionName("CallNewElevator") 152 | \/ \E e \in Elevator : OpenElevatorDoors(e) /\ ActionName("OpenElevatorDoors") 153 | \/ \E e \in Elevator : EnterElevator(e) /\ ActionName("EnterElevator") 154 | \/ \E e \in Elevator : ExitElevator(e) /\ ActionName("ExitElevator") 155 | \/ \E e \in Elevator : CloseElevatorDoors(e) /\ ActionName("CloseElevatorDoors") 156 | \/ \E e \in Elevator : MoveElevator(e) /\ ActionName("MoveElevator") 157 | \/ \E e \in Elevator : StopElevator(e) /\ ActionName("StopElevator") 158 | \/ \E c \in ElevatorCall : DispatchElevator(c) /\ ActionName("DispatchElevator") 159 | 160 | AnimSpec == 161 | /\ AnimatedInit(ReducedInit, View) 162 | /\ [][AnimatedNext(AnnotatedNext, View, TRUE)]_<> 163 | 164 | ==================================================================================================== 165 | \* Modification History 166 | \* Last modified Sat Jul 07 16:26:03 EDT 2018 by williamschultz 167 | \* Created Fri Mar 23 00:50:27 EDT 2018 by williamschultz 168 | -------------------------------------------------------------------------------- /examples/LogVisibility/LogVisibility.tla: -------------------------------------------------------------------------------- 1 | ------------------------------------- MODULE LogVisibility ------------------------------------- 2 | EXTENDS Integers, TLC 3 | 4 | \* 5 | \* A very simple model illustrating the concept of transaction log visibility. The "log" in this model is a sequence 6 | \* of operations (transactions), but each 'slot' of the log may be written to concurrently. Concurrent transactions are 7 | \* assigned timestamps (slots ids in this model), which defines their slot position. Since timestamp order 8 | \* does not necessarily reflect real time commit order, there must be some mechanism for ensuring that reads up 9 | \* to a certain position in the log do not return sections of the log with "holes". To do this, we enforce 10 | \* a log "visibility" rule, which says that you are not allowed to read up to a certain slot of the log unitl 11 | \* it is known that the state of all previous slots are "set in stone" i.e. they will never change. 12 | \* These concepts are expressed more precisely below. 13 | \* 14 | 15 | \* A monotonically increasing integral counter. 16 | VARIABLE clock 17 | 18 | \* A function mapping slot ids to "slots". Each slot represents a position 19 | \* in the log that can either be "filled" by an operation, or left empty. 20 | \* Every slot is uniquely identified by its id. In a real system, these slot ids 21 | \* could likely be timestamps. 22 | VARIABLE slots 23 | 24 | \* For demonstration, we include the "visibility" point as an explicit variable in the model. This is maximum 25 | \* slot at which it is valid to read from. 26 | VARIABLE maxVisibleSlot 27 | 28 | Maximum(S) == CHOOSE x \in S : \A y \in S : x >= y 29 | 30 | \* Expression determining whether a particular slot 's' should be considered "visible". This means 31 | \* that it is valid to read at this slot position. All slots earlier than it must have either 32 | \* committed or aborted. 33 | Visible(i) == 34 | /\ i \in DOMAIN slots 35 | /\ \A s \in DOMAIN slots : s <= i => slots[s].state \in {"Committed", "Aborted"} 36 | 37 | MaxVisibleSlot == 38 | LET visibleSlots == {i \in DOMAIN slots : Visible(i)} IN 39 | IF visibleSlots = {} THEN -1 ELSE Maximum(visibleSlots) 40 | 41 | StartTxn == 42 | /\ clock' = clock + 1 43 | /\ slots' = [slots EXCEPT ![clock] = [state |-> "Pending"]] 44 | /\ maxVisibleSlot' = MaxVisibleSlot' 45 | 46 | CommitTxn(i) == 47 | /\ slots[i].state = "Pending" 48 | /\ slots' = [slots EXCEPT ![i] = [state |-> "Committed"]] 49 | /\ maxVisibleSlot' = MaxVisibleSlot' 50 | /\ UNCHANGED clock 51 | 52 | AbortTxn(i) == 53 | /\ slots[i].state = "Pending" 54 | /\ slots' = [slots EXCEPT ![i] = [state |-> "Aborted"]] 55 | /\ maxVisibleSlot' = MaxVisibleSlot' 56 | /\ UNCHANGED clock 57 | 58 | Init == 59 | /\ clock = 0 60 | \* All slots are initially unused. If a slot id doesn't exist in the domain of 'slots', 61 | \* it means that that slot is available. 62 | /\ slots = [n \in Nat |-> [state |-> "Available"]] 63 | /\ maxVisibleSlot = -1 64 | 65 | Next == 66 | \/ StartTxn 67 | \/ \E i \in DOMAIN slots : CommitTxn(i) 68 | \/ \E i \in DOMAIN slots : AbortTxn(i) 69 | 70 | ==================================================================================================== 71 | \* Modification History 72 | \* Last modified Wed May 09 21:06:34 EDT 2018 by williamschultz 73 | \* Created Wed May 09 19:47:13 EDT 2018 by williamschultz 74 | -------------------------------------------------------------------------------- /examples/LogVisibility/LogVisibilityAnimated.tla: -------------------------------------------------------------------------------- 1 | ---------------------------------- MODULE LogVisibilityAnimated ---------------------------------- 2 | EXTENDS Integers, Animation, LogVisibility 3 | 4 | States == {"Available", "Pending", "Committed", "Aborted"} 5 | 6 | StateColor(state) == 7 | CASE state = "Available" -> "lightgray" 8 | [] state = "Pending" -> "yellow" 9 | [] state = "Committed" -> "green" 10 | [] state = "Aborted" -> "red" 11 | [] OTHER -> "gray" 12 | 13 | SlotDims == [w |-> 90, h |-> 90] 14 | SlotColor(i) == StateColor(slots[i].state) 15 | 16 | SlotRect(i) == 17 | LET label == Text(i * (SlotDims.w + 5) + 20, 20, ToString(i), [font |-> "Arial"]) 18 | rect == Rect(i * (SlotDims.w + 5), 30, SlotDims.w, SlotDims.h, [fill |-> SlotColor(i)]) IN 19 | Group(<>, <<>>) 20 | 21 | SlotRects == {SlotRect(i) : i \in DOMAIN slots} 22 | 23 | SlotNumber(i) == Text((maxVisibleSlot + 1) * (SlotDims.w + 5), 130, "Visibility Point", [font |-> "Arial"]) 24 | 25 | VisibilityPoint == Rect((maxVisibleSlot + 1) * (SlotDims.w + 5), 145, 3, 20, [fill |-> "orange"]) 26 | VisibilityText == Text((maxVisibleSlot + 1) * (SlotDims.w + 5), 180, "Visibility Point=" \o ToString(maxVisibleSlot), [font |-> "Arial"]) 27 | 28 | LegendState(k, state) == 29 | LET rect == Rect(5, k*30, 20, 20, [fill |-> StateColor(state)]) 30 | label == Text(35, (k*30 + 15), state, [font |-> "Arial"]) IN 31 | Group(<>, ("transform" :> "translate(800 190)")) 32 | 33 | Legend == LET StateSeq == SetToSeq(States) IN 34 | {LegendState(i, StateSeq[i]) : i \in DOMAIN StateSeq} 35 | 36 | Elems == SlotRects \cup {VisibilityPoint, VisibilityText} \cup Legend 37 | 38 | View == Group(SetToSeq(Elems), ("transform" :> "translate(80 50)")) 39 | 40 | vars == <> 41 | 42 | AnnotatedNext == 43 | \/ StartTxn /\ ActionName("StartTxn(" \o ToString(clock) \o ")") 44 | \/ \E i \in DOMAIN slots : CommitTxn(i) /\ ActionName("CommitTxn(" \o ToString(i) \o ")") 45 | \/ \E i \in DOMAIN slots : AbortTxn(i) /\ ActionName("AbortTxn(" \o ToString(i) \o ")") 46 | 47 | AnimSpec == 48 | /\ AnimatedInit(Init, View) 49 | /\ [][AnimatedNext(AnnotatedNext, View, TRUE)]_<> 50 | 51 | ---- 52 | 53 | \* Helpful definitions for model. 54 | 55 | MaxClock==6 56 | 57 | \* Can override the definition of 'Nat' with 'ModelNat' 58 | ModelNat == 0..MaxClock 59 | 60 | \* State constraint to make state space finite. 61 | StateConstraint == clock <= MaxClock 62 | 63 | 64 | ==================================================================================================== 65 | \* Modification History 66 | \* Last modified Sat Jul 14 15:31:43 EDT 2018 by williamschultz 67 | \* Created Wed May 09 20:38:43 EDT 2018 by williamschultz 68 | -------------------------------------------------------------------------------- /examples/LogVisibility/log_visibility.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TLA+ Trace Visualizer 5 | 6 | 7 | 8 | 9 | 36 | 37 |
38 | 39 | 40 |

Log Visibility

41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 01234567AvailablePendingCommittedAbortedVisibility Point=-1Next Action: Init01234567AvailablePendingCommittedAbortedVisibility Point=-1Next Action: StartTxn(0)01234567AvailablePendingCommittedAbortedVisibility Point=-1Next Action: StartTxn(1)01234567AvailablePendingCommittedAbortedVisibility Point=-1Next Action: CommitTxn(1)01234567AvailablePendingCommittedAbortedVisibility Point=-1Next Action: AbortTxn(0)01234567AvailablePendingCommittedAbortedVisibility Point=1Next Action: StartTxn(2)01234567AvailablePendingCommittedAbortedVisibility Point=1Next Action: CommitTxn(2)01234567AvailablePendingCommittedAbortedVisibility Point=2Next Action: StartTxn(3)01234567AvailablePendingCommittedAbortedVisibility Point=2Next Action: StartTxn(4)01234567AvailablePendingCommittedAbortedVisibility Point=2Next Action: CommitTxn(3)01234567AvailablePendingCommittedAbortedVisibility Point=3Next Action: StartTxn(5)01234567AvailablePendingCommittedAbortedVisibility Point=3Next Action: AbortTxn(5)01234567AvailablePendingCommittedAbortedVisibility Point=3Next Action: CommitTxn(4)01234567AvailablePendingCommittedAbortedVisibility Point=5Next Action: StartTxn(6)01234567AvailablePendingCommittedAbortedVisibility Point=5Next Action: CommitTxn(6)" 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
70 | 71 | 72 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /examples/Raft/RaftAnimated.tla: -------------------------------------------------------------------------------- 1 | --------------------------------------- MODULE RaftAnimated --------------------------------------- 2 | 3 | EXTENDS Animation, Integers, RaftSimpler 4 | 5 | (*** Generic Helpers ***) 6 | 7 | Injective(f) == \A x, y \in DOMAIN f : f[x] = f[y] => x = y 8 | 9 | \* Linearly interpolate a position halfway between two given points. 't', the interpolation factor, is measured 10 | \* on a scale from 0 to 100. 11 | Interp(p1, p2, t) == 12 | [ x |-> (t * p1.x + (100-t) * p2.x) \div 100 , 13 | y |-> (t * p1.y + (100-t) * p2.y) \div 100] 14 | 15 | \* Translate a point by a specified amount. 16 | Move(p, dx, dy) == [x |-> p.x + dx, y |-> p.y + dy] 17 | 18 | \* Reverse a sequence. 19 | Reverse(s) == [ i \in DOMAIN s |-> s[Len(s)-i+1]] 20 | 21 | (*** Server Elements ***) 22 | 23 | \* Establish a fixed mapping to assign an ordering to elements in these sets. 24 | ServerId == CHOOSE f \in [Server -> 1..Cardinality(Server)] : Injective(f) 25 | 26 | \* This animation assumes 3 servers. 27 | Base == [x |-> 450, y |-> 100] 28 | ServerPositions == 29 | 1 :> [x |-> Base.x, y |-> Base.y] @@ 30 | 2 :> [x |-> Base.x - 200, y |-> Base.y + 300] @@ 31 | 3 :> [x |-> Base.x + 200, y |-> Base.y + 300] 32 | 33 | \* Colors to represent the state of a server. 34 | StateColor == 35 | Leader :> "green" @@ 36 | Candidate :> "yellow" @@ 37 | Follower :> "darkgray" 38 | 39 | \* Generates a graphic element representing server 'sid'. 40 | ServerElem(sid) == 41 | LET pos == ServerPositions[ServerId[sid]] 42 | circleAttrs == ("stroke" :> "black" @@ "stroke-width" :> "1" @@ "fill" :> StateColor[state[sid]]) 43 | circle == Circle(pos.x, pos.y, 27, circleAttrs) 44 | termLabel == Text(pos.x, pos.y + 45, ToString(currentTerm[sid]), [fill |-> "red"]) 45 | serverLabel == Text(pos.x - 7, pos.y - 35, "n" \o ToString(sid), [fill |-> "black"]) IN 46 | Group(<>, <<>>) 47 | 48 | VotesGranted(s) == 49 | LET pos == ServerPositions[ServerId[s]] IN 50 | Text(pos.x - 15, pos.y + 75, "votes: " \o ToString(votesGranted[s]), ("font-size" :> "11")) 51 | 52 | ServerElems == {Group(<>, <<>>) : s \in Server} 53 | 54 | (*** Message Elements ***) 55 | 56 | \* Constructs a message element at position 'pos'. 57 | MessageElem(m, pos) == 58 | LET text == 59 | CASE m.mtype = RequestVoteRequest -> 60 | "VoteRequest" \o " (" \o 61 | Join( << Join(<<"term", ToString(m.mterm)>>, "="), 62 | Join(<<"from", ToString(m.msource)>>, "=") >>, ", ") \o ")" 63 | [] m.mtype = RequestVoteResponse -> 64 | "VoteResponse" \o " (" \o 65 | Join( << Join(<<"term", ToString(m.mterm)>>, "="), 66 | Join(<<"from", ToString(m.msource)>>, "="), 67 | Join(<<"granted", ToString(m.mvoteGranted)>>, "=") >>, ", ") \o ")" 68 | [] m.mtype = AppendEntriesRequest -> 69 | "AppendEntries" \o " (" \o 70 | Join( << Join(<<"term", ToString(m.mterm)>>, "="), 71 | ToString(m.mentries) >>, ", ") \o ")" 72 | [] OTHER -> ToString(m.mtype) IN 73 | Text(pos.x, pos.y, text, ("font-size" :> "12")) 74 | 75 | \* Produces a group of graphic elements for a given set of messages. 76 | MessageList(msgs, basePos) == 77 | IF msgs = {} THEN Group(<<>>, <<>>) ELSE 78 | LET msgList == Reverse(SetToSeq(msgs)) 79 | msgElems == {MessageElem(msgList[i], [x |-> basePos.x, y |-> basePos.y + (i*15)]) : i \in DOMAIN msgList} IN 80 | Group(SetToSeq(msgElems), <<>>) 81 | 82 | \* The set of all messages in flight to a particular server. 83 | IncomingMessages(sid) == {m \in DOMAIN messages : m.mdest = sid /\ messages[m] > 0} 84 | 85 | MessageElems == {MessageList(IncomingMessages(s), Move(ServerPositions[ServerId[s]], 35, 10)) : s \in Server} 86 | 87 | \* The position of a log slot for a server and log index. 88 | LogEntryDims == [w |-> 30, h |-> 30] 89 | LogPos(s, i) == [x |-> 780 + i * (LogEntryDims.w + 3) , y |-> 100 + ServerId[s] * (LogEntryDims.h + 3)] 90 | 91 | \* Generate a single log entry element at index 'i' for server 'sid'. 92 | LogEntryElem(sid, i) == 93 | LET pos == LogPos(sid, i) 94 | entry == Rect(pos.x, pos.y, LogEntryDims.w, LogEntryDims.h, [fill |-> "lightblue"]) 95 | term == Text((pos.x + LogEntryDims.w \div 2 - 6), (pos.y + LogEntryDims.h \div 2 + 8), ToString(log[sid][i].term), ("font-size" :> "18")) IN 96 | Group(<>, <<>>) 97 | 98 | LogElem(sid) == 99 | LET entryElems == {LogEntryElem(sid, i) : i \in DOMAIN log[sid]} IN 100 | Group(SetToSeq(entryElems), <<>>) 101 | 102 | LogElems == {LogElem(s) : s \in Server} 103 | 104 | (*** Log Slot Elements ***) 105 | MaxLogSlots == 5 106 | LogSlot(sid, i) == Rect(LogPos(sid, i).x, LogPos(sid, i).y, LogEntryDims.w, LogEntryDims.h, ("stroke" :> "black" @@ "stroke-width" :> "1" @@ "fill" :> "white")) 107 | LogSlotElem(sid) == 108 | LET slotElems == {LogSlot(sid, i) : i \in 1..MaxLogSlots} IN 109 | Group(SetToSeq(slotElems), <<>>) 110 | LogSlotElems == {LogSlotElem(s) : s \in Server} 111 | 112 | LogLabel(s) == Text((LogPos(s, 0).x-12), (LogPos(s, 0).y + 16), ToString(s), ("font-size" :> "16")) 113 | LogLabels == {LogLabel(s) : s \in Server} 114 | 115 | \* All graphic elements. 116 | AllElems == SetToSeq(ServerElems) \o 117 | SetToSeq(MessageElems) \o 118 | SetToSeq(LogSlotElems) \o 119 | SetToSeq(LogElems) \o 120 | SetToSeq(LogLabels) 121 | 122 | 123 | View == Group(AllElems, <<>>) 124 | 125 | AnimatedSpec == \* Initialize state with Init and transition with Next, subject to TemporalAssumptions 126 | /\ AnimatedInit(Init, View) 127 | /\ [][AnimatedNext(Next, View, FALSE)]_<> 128 | 129 | (****************************************************) 130 | (* A pre-defined trace that we want to animate. *) 131 | (****************************************************) 132 | 133 | TraceInit == Init 134 | 135 | TraceNext == 136 | /\ allLogs' = allLogs \cup {log[i] : i \in Server} 137 | /\ \* Let node 0 become leader in term 2. 138 | \/ Timeout(0) /\ currentTerm[0] = 1 139 | \/ \E j \in Server : RequestVote(0, j) /\ currentTerm[0] = 1 140 | \/ BecomeLeader(0) 141 | \/ ClientRequest(0, "v1") /\ currentTerm[0] = 2 142 | 143 | \* Let node 2 become leader in term 3. 144 | \/ Timeout(2) /\ currentTerm[2] = 2 /\ currentTerm[0] = 2 /\ state[0] = Leader /\ Len(log[0]) > 0 145 | \/ RequestVote(2, 1) /\ currentTerm[2] = 3 146 | \/ RequestVote(2, 2) /\ currentTerm[2] = 3 147 | \/ BecomeLeader(2) 148 | \/ ClientRequest(2, "v1") /\ currentTerm[2] = 3 149 | \/ AppendEntries(2, 1) /\ currentTerm[0] = 3 150 | 151 | \* Let node 0 become leader again in term 4. 152 | \/ Timeout(0) /\ currentTerm[0] = 3 /\ currentTerm[2] = 3 /\ state[2] = Leader /\ Len(log[2]) > 0 153 | \/ \E j \in Server : RequestVote(0, j) 154 | \/ BecomeLeader(0) 155 | \/ ClientRequest(0, "v1") /\ currentTerm[0] = 4 156 | \/ AppendEntries(0, 1) /\ currentTerm[0] = 4 157 | 158 | \* Node2 becomes leader once again, in term 5. 159 | \/ Restart(0) /\ currentTerm[0] = 4 /\ state[0] = Leader /\ Len(log[1]) > 0 160 | \/ Timeout(2) /\ currentTerm[0] = 4 /\ state[0] = Leader /\ Len(log[1]) > 0 161 | \/ RequestVote(2, 1) /\ currentTerm[2] = 5 162 | \/ RequestVote(2, 2) /\ currentTerm[2] = 5 163 | \/ BecomeLeader(2) /\ currentTerm[2] = 5 164 | 165 | \* New leader replicates an entry that overwrites the first log entry on node 1. 166 | \/ AppendEntries(2, 1) /\ currentTerm[2] = 5 167 | \/ ClientRequest(2, "v1") /\ currentTerm[2] = 5 168 | \/ AdvanceCommitIndex(2) /\ currentTerm[2] = 5 169 | 170 | \/ \E m \in DOMAIN messages : 171 | /\ messages[m] > 0 \* Message must actually be in transit. 172 | /\ Receive(m) 173 | 174 | TraceAnimatedSpec == \* Initialize state with Init and transition with Next, subject to TemporalAssumptions 175 | /\ AnimatedInit(TraceInit, View) 176 | /\ [][AnimatedNext(TraceNext, View, FALSE)]_<> 177 | 178 | ==================================================================================================== 179 | \* Modification History 180 | \* Last modified Sun Jul 08 12:24:21 EDT 2018 by williamschultz 181 | \* Created Sun Mar 25 12:40:58 EDT 2018 by williamschultz 182 | -------------------------------------------------------------------------------- /examples/Raft/RaftSimpler.tla: -------------------------------------------------------------------------------- 1 | ---------------------------------------- MODULE RaftSimpler ---------------------------------------- 2 | \* The goal of this spec is to fix any bugs in the original Raft spec and simplify any parts of it 3 | \* where possible to reduce the state space so as to aid model checking. Based on the original Raft 4 | \* spec: https://github.com/ongardie/raft.tla 5 | 6 | EXTENDS Naturals, FiniteSets, Sequences, TLC, Bags 7 | 8 | \* The set of server IDs 9 | CONSTANTS Server 10 | 11 | \* The set of requests that can go into the log 12 | CONSTANTS Value 13 | 14 | \* Server states. 15 | CONSTANTS Follower, Candidate, Leader 16 | 17 | \* A reserved value. 18 | CONSTANTS Nil 19 | 20 | \* Message types: 21 | CONSTANTS RequestVoteRequest, RequestVoteResponse, 22 | AppendEntriesRequest, AppendEntriesResponse 23 | 24 | ---- 25 | \* Global variables 26 | 27 | \* A bag of records representing requests and responses sent from one server 28 | \* to another. TLAPS doesn't support the Bags module, so this is a function 29 | \* mapping Message to Nat. 30 | VARIABLE messages 31 | 32 | \* A history variable used in the proof. This would not be present in an 33 | \* implementation. 34 | \* Keeps track of successful elections, including the initial logs of the 35 | \* leader and voters' logs. Set of functions containing various things about 36 | \* successful elections (see BecomeLeader). 37 | VARIABLE elections 38 | 39 | \* A history variable used in the proof. This would not be present in an 40 | \* implementation. 41 | \* Keeps track of every log ever in the system (set of logs). 42 | VARIABLE allLogs 43 | 44 | ---- 45 | \* The following variables are all per server (functions with domain Server). 46 | 47 | \* The server's term number. 48 | VARIABLE currentTerm 49 | \* The server's state (Follower, Candidate, or Leader). 50 | VARIABLE state 51 | \* The candidate the server voted for in its current term, or 52 | \* Nil if it hasn't voted for any. 53 | VARIABLE votedFor 54 | serverVars == <> 55 | 56 | \* A Sequence of log entries. The index into this sequence is the index of the 57 | \* log entry. Unfortunately, the Sequence module defines Head(s) as the entry 58 | \* with index 1, so be careful not to use that! 59 | VARIABLE log 60 | \* The index of the latest entry in the log the state machine may apply. 61 | VARIABLE commitIndex 62 | logVars == <> 63 | 64 | \* The following variables are used only on candidates: 65 | \* The set of servers from which the candidate has received a RequestVote 66 | \* response in its currentTerm. 67 | VARIABLE votesResponded 68 | \* The set of servers from which the candidate has received a vote in its 69 | \* currentTerm. 70 | VARIABLE votesGranted 71 | \* A history variable used in the proof. This would not be present in an 72 | \* implementation. 73 | \* Function from each server that voted for this candidate in its currentTerm 74 | \* to that voter's log. 75 | VARIABLE voterLog 76 | candidateVars == <> 77 | 78 | \* The following variables are used only on leaders: 79 | \* The next entry to send to each follower. 80 | VARIABLE nextIndex 81 | \* The latest entry that each follower has acknowledged is the same as the 82 | \* leader's. This is used to calculate commitIndex on the leader. 83 | VARIABLE matchIndex 84 | leaderVars == <> 85 | 86 | \* End of per server variables. 87 | 88 | VARIABLE timeouts 89 | 90 | ---- 91 | 92 | \* All variables; used for stuttering (asserting state hasn't changed). 93 | vars == <> 94 | 95 | ---- 96 | \* Helpers 97 | 98 | \* Range of a function, e.g. elements of a sequence 99 | Range(f) == { f[x] : x \in DOMAIN f } 100 | 101 | \* The set of all quorums. This just calculates simple majorities, but the only 102 | \* important property is that every quorum overlaps with every other. 103 | Quorum == {i \in SUBSET(Server) : Cardinality(i) * 2 > Cardinality(Server)} 104 | 105 | \* The term of the last entry in a log, or 0 if the log is empty. 106 | LastTerm(xlog) == IF Len(xlog) = 0 THEN 0 ELSE xlog[Len(xlog)].term 107 | 108 | \* Helper for Send and Reply. Given a message m and bag of messages, return a 109 | \* new bag of messages with one more m in it. 110 | WithMessage(m, msgs) == 111 | IF m \in DOMAIN msgs THEN 112 | [msgs EXCEPT ![m] = msgs[m] + 1] 113 | ELSE 114 | msgs @@ (m :> 1) 115 | 116 | \* Helper for Discard and Reply. Given a message m and bag of messages, return 117 | \* a new bag of messages with one less m in it. 118 | WithoutMessage(m, msgs) == 119 | IF m \in DOMAIN msgs THEN 120 | [msgs EXCEPT ![m] = msgs[m] - 1] 121 | ELSE 122 | msgs 123 | 124 | \* Add a message to the bag of messages. 125 | Send(m) == messages' = WithMessage(m, messages) 126 | 127 | \* Add a message to the bag of messages, only if there is not already a 128 | \* copy of it in transit. 129 | WithMessageNoDups(m, msgs) == 130 | IF m \in DOMAIN msgs THEN 131 | [msgs EXCEPT ![m] = 1] 132 | ELSE 133 | msgs @@ (m :> 1) 134 | 135 | MessageExists(m) == m \in DOMAIN messages /\ messages[m] > 0 136 | 137 | SendNoDups(m) == 138 | /\ ~MessageExists(m) 139 | /\ messages' = WithMessageNoDups(m, messages) 140 | 141 | \* Remove a message from the bag of messages. Used when a server is done 142 | \* processing a message. 143 | Discard(m) == messages' = WithoutMessage(m, messages) 144 | 145 | \* Combination of Send and Discard 146 | Reply(response, request) == 147 | messages' = WithoutMessage(request, WithMessageNoDups(response, messages)) 148 | 149 | \* Return the minimum value from a set, or undefined if the set is empty. 150 | Min(s) == CHOOSE x \in s : \A y \in s : x <= y 151 | \* Return the maximum value from a set, or undefined if the set is empty. 152 | Max(s) == CHOOSE x \in s : \A y \in s : x >= y 153 | 154 | -------- 155 | 156 | (**************************************************************************************************) 157 | (* Properties & Definitions for Verification *) 158 | (**************************************************************************************************) 159 | 160 | \* An entry (index, term) is "immediately committed" at term t if it is 161 | \* acknowledged by a quorum of servers during term. This is a state predicate, and 162 | \* so is only true or false of a single state. Thus, it is possible that this property is 163 | \* "transient", in that it is only temporarily true. For an entry to be "immediately committed", 164 | \* however, even transiently, should imply that it is "committed", which is a persistent property i.e. 165 | \* it should hold true in all future states. 166 | IsImmediatelyCommitted(index, term, t) == 167 | /\ \exists leader \in Server : 168 | \exists subquorum \in SUBSET Server : 169 | /\ subquorum \cup {leader} \in Quorum 170 | /\ \A i \in subquorum : 171 | \E m \in DOMAIN messages : 172 | /\ messages[m] > 0 173 | /\ m.mtype = AppendEntriesResponse 174 | /\ m.msource = i 175 | /\ m.mdest = leader 176 | /\ m.mterm = term 177 | /\ m.mmatchIndex \geq index 178 | \* /\ state[i] = Follower 179 | \* /\ \E k \in DOMAIN log[i] : <> = <> 180 | \* /\ currentTerm[i] = t 181 | 182 | \* All (index,term) pairs from any log. 183 | allIndicesTerms == UNION { {<> : i \in DOMAIN log[s]} : s \in Server} 184 | 185 | ImmediatelyCommitted == { indexTerm \in allIndicesTerms : 186 | IsImmediatelyCommitted(indexTerm[1], indexTerm[2], indexTerm[2]) } 187 | 188 | 189 | \* Is <> present in the given log 'L' 190 | EntryInLog(index, term, L) == \E i \in DOMAIN L: <> = <> 191 | 192 | \* An entry (index, term) is considered committed at term t if it is present 193 | \* in the log of every leader with term > t 194 | Committed(index, term, t) == 195 | \A election \in elections : 196 | election.eterm > t => EntryInLog(index, term, election.elog) 197 | 198 | 199 | \* Asserts the existence of an (index, term) pair that exists in 200 | \* a majority of server logs. 201 | \*OnMajority(index, term) == 202 | \* \E majority \in Quorum : 203 | \* \A s \in majority : 204 | \* /\ EntryInLog(index, term, log[s]) 205 | \* /\ \E t \in majority : ~EntryInLog(index, term, log'[s]) 206 | 207 | RollbackMajority == 208 | \E indexTerm \in allIndicesTerms : 209 | \E majority \in Quorum : 210 | LET index == indexTerm[1] 211 | term == indexTerm[2] IN 212 | \* The log entry exists on a majority of servers. 213 | /\ \A s \in majority : EntryInLog(index, term, log[s]) 214 | \* There exists some server in the majority that no longer contains 215 | \* the log entry in the next state. 216 | /\ \E s \in majority : ~EntryInLog(index, term, log'[s]) 217 | 218 | 219 | LeaderCompleteness == \A indexTerm \in ImmediatelyCommitted : 220 | Committed(indexTerm[1], indexTerm[2], indexTerm[2]) 221 | 222 | \* A primary has sent an AppendEntries request containing a log 223 | \* entry with a term older than its current term. 224 | AppendEntryWithOldTerm == 225 | \E m \in DOMAIN messages : 226 | /\ messages[m] > 0 227 | /\ m.mtype = AppendEntriesRequest 228 | /\ \E entry \in Range(m.mentries) : entry.term < m.mterm 229 | 230 | -------- 231 | 232 | \* Define initial values for all variables 233 | 234 | InitHistoryVars == /\ elections = {} 235 | /\ allLogs = {} 236 | /\ voterLog = [i \in Server |-> [j \in {} |-> <<>>]] 237 | InitServerVars == /\ currentTerm = [i \in Server |-> 1] 238 | /\ state = [i \in Server |-> Follower] 239 | /\ votedFor = [i \in Server |-> Nil] 240 | InitCandidateVars == /\ votesResponded = [i \in Server |-> {}] 241 | /\ votesGranted = [i \in Server |-> {}] 242 | \* The values nextIndex[i][i] and matchIndex[i][i] are never read, since the 243 | \* leader does not send itself messages. It's still easier to include these 244 | \* in the functions. 245 | InitLeaderVars == /\ nextIndex = [i \in Server |-> [j \in Server |-> 1]] 246 | /\ matchIndex = [i \in Server |-> [j \in Server |-> 0]] 247 | InitLogVars == /\ log = [i \in Server |-> << >>] 248 | /\ commitIndex = [i \in Server |-> 0] 249 | Init == /\ messages = [m \in {} |-> 0] 250 | /\ timeouts = 0 251 | /\ InitHistoryVars 252 | /\ InitServerVars 253 | /\ InitCandidateVars 254 | /\ InitLeaderVars 255 | /\ InitLogVars 256 | 257 | ---- 258 | \* Define state transitions 259 | 260 | \* Server i restarts from stable storage. 261 | \* It loses everything but its currentTerm, votedFor, and log. 262 | Restart(i) == 263 | /\ state' = [state EXCEPT ![i] = Follower] 264 | /\ votesResponded' = [votesResponded EXCEPT ![i] = {}] 265 | /\ votesGranted' = [votesGranted EXCEPT ![i] = {}] 266 | /\ voterLog' = [voterLog EXCEPT ![i] = [j \in {} |-> <<>>]] 267 | /\ nextIndex' = [nextIndex EXCEPT ![i] = [j \in Server |-> 1]] 268 | /\ matchIndex' = [matchIndex EXCEPT ![i] = [j \in Server |-> 0]] 269 | /\ commitIndex' = [commitIndex EXCEPT ![i] = 0] 270 | /\ UNCHANGED <> 271 | 272 | \* Server i times out and starts a new election. 273 | 274 | \* Note: see if we can limit the amount of timeouts that occur during model checking 275 | \* to limit state space (WSchultz, April 28, 2018) 276 | Timeout(i) == /\ state[i] \in {Follower, Candidate} 277 | /\ state' = [state EXCEPT ![i] = Candidate] 278 | /\ currentTerm' = [currentTerm EXCEPT ![i] = currentTerm[i] + 1] 279 | \* Most implementations would probably just set the local vote 280 | \* atomically, but messaging localhost for it is weaker. 281 | /\ votedFor' = [votedFor EXCEPT ![i] = Nil] 282 | \* Vote for yourself right away (WSchultz). 283 | /\ votesResponded' = [votesResponded EXCEPT ![i] = {i}] 284 | /\ votesGranted' = [votesGranted EXCEPT ![i] = {i}] 285 | /\ voterLog' = [voterLog EXCEPT ![i] = [j \in {} |-> <<>>]] 286 | /\ timeouts' = timeouts + 1 287 | /\ UNCHANGED <> 288 | 289 | \* Candidate i sends j a RequestVote request. 290 | RequestVote(i, j) == 291 | /\ state[i] = Candidate 292 | /\ j \notin votesResponded[i] 293 | /\ SendNoDups([ mtype |-> RequestVoteRequest, 294 | mterm |-> currentTerm[i], 295 | mlastLogTerm |-> LastTerm(log[i]), 296 | mlastLogIndex |-> Len(log[i]), 297 | msource |-> i, 298 | mdest |-> j]) 299 | /\ UNCHANGED <> 300 | 301 | 302 | \* Leader i sends j an AppendEntries request containing up to 1 entry. 303 | \* While implementations may want to send more than 1 at a time, this spec uses 304 | \* just 1 because it minimizes atomic regions without loss of generality. 305 | AppendEntries(i, j) == 306 | /\ i /= j 307 | /\ state[i] = Leader 308 | /\ LET prevLogIndex == nextIndex[i][j] - 1 309 | prevLogTerm == IF prevLogIndex > 0 THEN 310 | log[i][prevLogIndex].term 311 | ELSE 312 | 0 313 | \* Send up to 1 entry, constrained by the end of the log. 314 | lastEntry == Min({Len(log[i]), nextIndex[i][j]}) 315 | entries == SubSeq(log[i], nextIndex[i][j], lastEntry) 316 | \* Since this spec isn't worried about liveness properties, it doesn't 317 | \* seem absolutely necessary to send AppendEntries messages with 0 entries. (WSchultz, April 30, 2018). 318 | IN /\ Len(entries) > 0 319 | /\ SendNoDups([mtype |-> AppendEntriesRequest, 320 | mterm |-> currentTerm[i], 321 | mprevLogIndex |-> prevLogIndex, 322 | mprevLogTerm |-> prevLogTerm, 323 | mentries |-> entries, 324 | \* mlog is used as a history variable for the proof. 325 | \* It would not exist in a real implementation. 326 | mlog |-> << >>, \*log[i], 327 | mcommitIndex |-> Min({commitIndex[i], lastEntry}), 328 | msource |-> i, 329 | mdest |-> j]) 330 | /\ UNCHANGED <> 331 | 332 | \* Candidate i transitions to leader. 333 | BecomeLeader(i) == 334 | /\ state[i] = Candidate 335 | /\ votesGranted[i] \in Quorum 336 | /\ state' = [state EXCEPT ![i] = Leader] 337 | /\ nextIndex' = [nextIndex EXCEPT ![i] = 338 | [j \in Server |-> Len(log[i]) + 1]] 339 | /\ matchIndex' = [matchIndex EXCEPT ![i] = 340 | [j \in Server |-> 0]] 341 | /\ elections' = elections \cup 342 | {[eterm |-> currentTerm[i], 343 | eleader |-> i, 344 | elog |-> log[i], 345 | evotes |-> votesGranted[i], 346 | evoterLog |-> voterLog[i]]} 347 | /\ UNCHANGED <> 348 | 349 | \* Leader i receives a client request to add v to the log. 350 | ClientRequest(i, v) == 351 | /\ state[i] = Leader 352 | \* Limit each leader to write only one log entry (WSchultz, May 6, 2018) 353 | /\ LastTerm(log[i]) # currentTerm[i] 354 | /\ LET entry == [term |-> currentTerm[i], 355 | value |-> v] 356 | newLog == Append(log[i], entry) 357 | IN log' = [log EXCEPT ![i] = newLog] 358 | /\ UNCHANGED <> 360 | 361 | \* Leader i advances its commitIndex. 362 | \* This is done as a separate step from handling AppendEntries responses, 363 | \* in part to minimize atomic regions, and in part so that leaders of 364 | \* single-server clusters are able to mark entries committed. 365 | AdvanceCommitIndex(i) == 366 | /\ state[i] = Leader 367 | /\ LET \* The set of servers that agree up through index. 368 | Agree(index) == {i} \cup {k \in Server : 369 | matchIndex[i][k] >= index} 370 | \* The maximum indexes for which a quorum agrees 371 | agreeIndexes == {index \in 1..Len(log[i]) : 372 | Agree(index) \in Quorum} 373 | \* New value for commitIndex'[i] 374 | newCommitIndex == 375 | IF /\ agreeIndexes /= {} 376 | /\ log[i][Max(agreeIndexes)].term = currentTerm[i] 377 | THEN 378 | Max(agreeIndexes) 379 | ELSE 380 | commitIndex[i] 381 | IN commitIndex' = [commitIndex EXCEPT ![i] = newCommitIndex] 382 | /\ UNCHANGED <> 383 | 384 | ---- 385 | \* Message handlers 386 | \* i = recipient, j = sender, m = message 387 | 388 | \* Server i receives a RequestVote request from server j with 389 | \* m.mterm <= currentTerm[i]. 390 | HandleRequestVoteRequest(i, j, m) == 391 | LET logOk == \/ m.mlastLogTerm > LastTerm(log[i]) 392 | \/ /\ m.mlastLogTerm = LastTerm(log[i]) 393 | /\ m.mlastLogIndex >= Len(log[i]) 394 | \* Can be used to introduce an artifical bug. This voting rule 395 | \* allows a node to vote as many times as they want for a given term. (Will Schultz) 396 | grantUnlimitedVotesPerTerm == 397 | /\ m.mterm = currentTerm[i] 398 | /\ logOk 399 | \* Can be used to introduce an artifical bug. This voting rule 400 | \* restricts a node to vote only once per term, but allows them to 401 | \* cast their vote for any candidate, even if the candidate's log isn't ahead of their own. (Will Schultz) 402 | grantVoteToAnyCandidate == 403 | /\ m.mterm = currentTerm[i] 404 | /\ votedFor[i] \in {Nil, j} 405 | grantCorrect == 406 | /\ m.mterm = currentTerm[i] 407 | /\ logOk 408 | /\ votedFor[i] \in {Nil, j} 409 | grant == grantCorrect \* Possible to artificially violate the voting rules here. (Will Schultz) 410 | IN /\ m.mterm <= currentTerm[i] 411 | /\ \/ grant /\ votedFor' = [votedFor EXCEPT ![i] = j] 412 | \/ ~grant /\ UNCHANGED votedFor 413 | /\ Reply([mtype |-> RequestVoteResponse, 414 | mterm |-> currentTerm[i], 415 | mvoteGranted |-> grant, 416 | \* mlog is used just for the `elections' history variable for 417 | \* the proof. It would not exist in a real implementation. 418 | mlog |-> << >>, \* log[i], 419 | msource |-> i, 420 | mdest |-> j], 421 | m) 422 | /\ UNCHANGED <> 423 | 424 | \* Server i receives a RequestVote response from server j with 425 | \* m.mterm = currentTerm[i]. 426 | HandleRequestVoteResponse(i, j, m) == 427 | \* This tallies votes even when the current state is not Candidate, but 428 | \* they won't be looked at, so it doesn't matter. 429 | /\ m.mterm = currentTerm[i] 430 | /\ votesResponded' = [votesResponded EXCEPT ![i] = 431 | votesResponded[i] \cup {j}] 432 | /\ \/ /\ m.mvoteGranted 433 | /\ votesGranted' = [votesGranted EXCEPT ![i] = 434 | votesGranted[i] \cup {j}] 435 | /\ voterLog' = voterLog \* [voterLog EXCEPT ![i] = voterLog[i] @@ (j :> m.mlog)] 436 | \/ /\ ~m.mvoteGranted 437 | /\ UNCHANGED <> 438 | /\ Discard(m) 439 | /\ UNCHANGED <> 440 | 441 | \* Server i receives an AppendEntries request from server j with 442 | \* m.mterm <= currentTerm[i]. This just handles m.entries of length 0 or 1, but 443 | \* implementations could safely accept more by treating them the same as 444 | \* multiple independent requests of 1 entry. 445 | 446 | HandleAppendEntriesRequest(i, j, m) == 447 | LET logOk == \/ m.mprevLogIndex = 0 448 | \/ /\ m.mprevLogIndex > 0 449 | /\ m.mprevLogIndex <= Len(log[i]) 450 | /\ m.mprevLogTerm = log[i][m.mprevLogIndex].term 451 | IN /\ m.mterm <= currentTerm[i] 452 | /\ \/ /\ \* reject request 453 | \/ m.mterm < currentTerm[i] \* Optionally don't reject only because of stale term. (Will Schultz) 454 | \/ /\ m.mterm = currentTerm[i] 455 | /\ state[i] = Follower 456 | /\ \lnot logOk 457 | /\ Reply([mtype |-> AppendEntriesResponse, 458 | mterm |-> currentTerm[i], 459 | msuccess |-> FALSE, 460 | mmatchIndex |-> 0, 461 | msource |-> i, 462 | mdest |-> j], 463 | m) 464 | /\ UNCHANGED <> 465 | \/ \* return to follower state 466 | /\ m.mterm = currentTerm[i] 467 | /\ state[i] = Candidate 468 | /\ state' = [state EXCEPT ![i] = Follower] 469 | /\ UNCHANGED <> 470 | \/ \* accept request 471 | /\ m.mterm = currentTerm[i] \* Optionally allow messages with stale terms (Will Schultz). 472 | /\ state[i] = Follower 473 | /\ logOk 474 | /\ LET index == m.mprevLogIndex + 1 475 | IN \/ \* already done with request 476 | /\ \/ m.mentries = << >> 477 | \/ \* Make sure we don't try to evaluate mentries[1] if its empty? (WSchultz) 478 | (/\ Len(m.mentries) > 0 479 | /\ Len(log[i]) >= index 480 | /\ log[i][index].term = m.mentries[1].term) 481 | \* This could make our commitIndex decrease (for 482 | \* example if we process an old, duplicated request), 483 | \* but that doesn't really affect anything. 484 | /\ commitIndex' = [commitIndex EXCEPT ![i] = 485 | m.mcommitIndex] 486 | /\ Reply([mtype |-> AppendEntriesResponse, 487 | mterm |-> currentTerm[i], 488 | msuccess |-> TRUE, 489 | mmatchIndex |-> m.mprevLogIndex + 490 | Len(m.mentries), 491 | msource |-> i, 492 | mdest |-> j], 493 | m) 494 | /\ UNCHANGED <> \* commitIndex is specified as changing above (WSchultz) 495 | \/ \* conflict: remove 1 entry 496 | /\ m.mentries /= << >> 497 | /\ Len(log[i]) >= index 498 | /\ log[i][index].term /= m.mentries[1].term 499 | /\ LET new == [index2 \in 1..(Len(log[i]) - 1) |-> 500 | log[i][index2]] 501 | IN log' = [log EXCEPT ![i] = new] 502 | /\ UNCHANGED <> 503 | \/ \* no conflict: append entry 504 | /\ m.mentries /= << >> 505 | /\ Len(log[i]) = m.mprevLogIndex 506 | /\ log' = [log EXCEPT ![i] = 507 | Append(log[i], m.mentries[1])] 508 | /\ UNCHANGED <> 509 | /\ UNCHANGED <> 510 | 511 | \* Server i receives an AppendEntries response from server j with 512 | \* m.mterm = currentTerm[i]. 513 | HandleAppendEntriesResponse(i, j, m) == 514 | /\ m.mterm = currentTerm[i] 515 | /\ \/ /\ m.msuccess \* successful 516 | /\ nextIndex' = [nextIndex EXCEPT ![i][j] = m.mmatchIndex + 1] 517 | /\ matchIndex' = [matchIndex EXCEPT ![i][j] = m.mmatchIndex] 518 | \/ /\ \lnot m.msuccess \* not successful 519 | /\ nextIndex' = [nextIndex EXCEPT ![i][j] = 520 | Max({nextIndex[i][j] - 1, 1})] 521 | /\ UNCHANGED <> 522 | /\ Discard(m) 523 | /\ UNCHANGED <> 524 | 525 | \* Any RPC with a newer term causes the recipient to advance its term first. 526 | UpdateTerm(i, j, m) == 527 | /\ m.mterm > currentTerm[i] 528 | /\ currentTerm' = [currentTerm EXCEPT ![i] = m.mterm] 529 | /\ state' = [state EXCEPT ![i] = Follower] 530 | /\ votedFor' = [votedFor EXCEPT ![i] = Nil] 531 | \* messages is unchanged so m can be processed further. 532 | /\ UNCHANGED <> 533 | 534 | \* Responses with stale terms are ignored. 535 | DropStaleResponse(i, j, m) == 536 | /\ m.mterm < currentTerm[i] 537 | /\ Discard(m) 538 | /\ UNCHANGED <> 539 | 540 | \* Receive a message. 541 | Receive(m) == 542 | LET i == m.mdest 543 | j == m.msource 544 | IN \* Any RPC with a newer term causes the recipient to advance 545 | \* its term first. Responses with stale terms are ignored. 546 | \/ UpdateTerm(i, j, m) 547 | \/ /\ m.mtype = RequestVoteRequest 548 | /\ HandleRequestVoteRequest(i, j, m) 549 | \/ /\ m.mtype = RequestVoteResponse 550 | /\ \/ DropStaleResponse(i, j, m) 551 | \/ HandleRequestVoteResponse(i, j, m) 552 | \/ /\ m.mtype = AppendEntriesRequest 553 | /\ HandleAppendEntriesRequest(i, j, m) 554 | \/ /\ m.mtype = AppendEntriesResponse 555 | /\ \/ DropStaleResponse(i, j, m) 556 | \/ HandleAppendEntriesResponse(i, j, m) 557 | 558 | \* End of message handlers. 559 | ---- 560 | \* Network state transitions 561 | 562 | \* The network duplicates a message 563 | DuplicateMessage(m) == 564 | /\ Send(m) 565 | /\ UNCHANGED <> 566 | 567 | \* The network drops a message 568 | DropMessage(m) == 569 | /\ Discard(m) 570 | /\ UNCHANGED <> 571 | 572 | ---- 573 | 574 | 575 | \* Defines how the variables may transition. 576 | Next == \* History variable that tracks every log ever: 577 | /\ allLogs' = allLogs \cup {log[i] : i \in Server} 578 | \* The core next state actions. 579 | /\ \/ \E i \in Server : Timeout(i) 580 | \/ \E i, j \in Server : RequestVote(i, j) 581 | \/ \E i \in Server : BecomeLeader(i) 582 | \/ \E i \in Server, v \in Value : ClientRequest(i, v) 583 | \/ \E i \in Server : AdvanceCommitIndex(i) 584 | \/ \E i,j \in Server : AppendEntries(i, j) 585 | \* This allows messages to be received if they are in DOMAIN messages, 586 | \* where messages is a bag (a function) that maps messages to counts. 587 | \* This seems like an error though. A message could exist in the DOMAIN of 588 | \* messages but map to 0, in which case it should not be valid to receive it. 589 | \* Changed the condition here to only accept messages that actually exist in the 590 | \* network. (WSchultz, April 30, 2018) 591 | \/ \E m \in DOMAIN messages : 592 | /\ messages[m] > 0 \* Message must actually be in transit. 593 | /\ Receive(m) 594 | 595 | 596 | \* Next state actions removed to speed up model checking. (Will Schultz) 597 | \* \/ \E i \in Server : Restart(i) 598 | \* \/ \E m \in DOMAIN messages : DuplicateMessage(m) 599 | \* \/ \E m \in DOMAIN messages : DropMessage(m) 600 | 601 | 602 | \* The specification must start with the initial state and transition according 603 | \* to Next. 604 | Spec == Init /\ [][Next]_vars /\ WF_vars(Next) 605 | 606 | --------------------------------------------------------------- 607 | 608 | 609 | ==================================================================================================== 610 | \* Modification History 611 | \* Last modified Sat Jun 23 15:08:48 EDT 2018 by williamschultz 612 | \* Created Sun Jun 10 17:46:01 EDT 2018 by williamschultz 613 | -------------------------------------------------------------------------------- /examples/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TLA+ Trace Visualizer 5 | 6 | 7 | 8 | 9 | 10 | 37 | 38 |
39 | 40 | 41 |

Title

42 | 43 | 44 | 45 | 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 | 57 | 58 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /new_examples/Elevator/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all generated/downloaded files. 2 | SpecTE.* 3 | TE.* 4 | viewExps.txt 5 | animation.html 6 | *.out 7 | *.jar 8 | states/ -------------------------------------------------------------------------------- /new_examples/Elevator/Elevator.tla: -------------------------------------------------------------------------------- 1 | ------------------------------------- MODULE Elevator ------------------------------------- 2 | (***************************************************************************) 3 | (* This spec describes a simple multi-car elevator system. The actions in *) 4 | (* this spec are unsurprising and common to all such systems except for *) 5 | (* DispatchElevator, which contains the logic to determine which elevator *) 6 | (* ought to service which call. The algorithm used is very simple and does *) 7 | (* not optimize for global throughput or average wait time. The *) 8 | (* TemporalInvariant definition ensures this specification provides *) 9 | (* capabilities expected of any elevator system, such as people eventually *) 10 | (* reaching their destination floor. *) 11 | (* *) 12 | (* Original spec: https://github.com/ahelwer/runway-tla-eval/Elevator.tla *) 13 | (***************************************************************************) 14 | 15 | EXTENDS Integers 16 | 17 | CONSTANTS Person, \* The set of all people using the elevator system 18 | Elevator, \* The set of all elevators 19 | FloorCount \* The number of floors serviced by the elevator system 20 | 21 | VARIABLES PersonState, \* The state of each person 22 | ActiveElevatorCalls, \* The set of all active elevator calls 23 | ElevatorState \* The state of each elevator 24 | 25 | Vars == \* Tuple of all specification variables 26 | <> 27 | 28 | Floor == \* The set of all floors 29 | 1 .. FloorCount 30 | 31 | Direction == \* Directions available to this elevator system 32 | {"Up", "Down"} 33 | 34 | ElevatorCall == \* The set of all elevator calls 35 | [floor : Floor, direction : Direction] 36 | 37 | ElevatorDirectionState == \* Elevator movement state; it is either moving in a direction or stationary 38 | Direction \cup {"Stationary"} 39 | 40 | GetDistance[f1, f2 \in Floor] == \* The distance between two floors 41 | IF f1 > f2 THEN f1 - f2 ELSE f2 - f1 42 | 43 | GetDirection[current, destination \in Floor] == \* Direction of travel required to move between current and destination floors 44 | IF destination > current THEN "Up" ELSE "Down" 45 | 46 | CanServiceCall[e \in Elevator, c \in ElevatorCall] == \* Whether elevator is in position to immediately service call 47 | LET eState == ElevatorState[e] IN 48 | /\ c.floor = eState.floor 49 | /\ c.direction = eState.direction 50 | 51 | PeopleWaiting[f \in Floor, d \in Direction] == \* The set of all people waiting on an elevator call 52 | {p \in Person : 53 | /\ PersonState[p].location = f 54 | /\ PersonState[p].waiting 55 | /\ GetDirection[PersonState[p].location, PersonState[p].destination] = d} 56 | 57 | TypeInvariant == \* Statements about the variables which we expect to hold in every system state 58 | /\ PersonState \in [Person -> [location : Floor \cup Elevator, destination : Floor, waiting : BOOLEAN]] 59 | /\ ActiveElevatorCalls \subseteq ElevatorCall 60 | /\ ElevatorState \in [Elevator -> [floor : Floor, direction : ElevatorDirectionState, doorsOpen : BOOLEAN, buttonsPressed : SUBSET Floor]] 61 | 62 | SafetyInvariant == \* Some more comprehensive checks beyond the type invariant 63 | /\ \A e \in Elevator : \* An elevator has a floor button pressed only if a person in that elevator is going to that floor 64 | /\ \A f \in ElevatorState[e].buttonsPressed : 65 | /\ \E p \in Person : 66 | /\ PersonState[p].location = e 67 | /\ PersonState[p].destination = f 68 | /\ \A p \in Person : \* A person is in an elevator only if the elevator is moving toward their destination floor 69 | /\ \A e \in Elevator : 70 | /\ (PersonState[p].location = e /\ ElevatorState[e].floor /= PersonState[p].destination) => 71 | /\ ElevatorState[e].direction = GetDirection[ElevatorState[e].floor, PersonState[p].destination] 72 | /\ \A c \in ActiveElevatorCalls : PeopleWaiting[c.floor, c.direction] /= {} \* No ghost calls 73 | 74 | TemporalInvariant == \* Expectations about elevator system capabilities 75 | /\ \A c \in ElevatorCall : \* Every call is eventually serviced by an elevator 76 | /\ c \in ActiveElevatorCalls ~> \E e \in Elevator : CanServiceCall[e, c] 77 | /\ \A p \in Person : \* If a person waits for their elevator, they'll eventually arrive at their floor 78 | /\ PersonState[p].waiting ~> PersonState[p].location = PersonState[p].destination 79 | 80 | PickNewDestination(p) == \* Person decides they need to go to a different floor 81 | LET pState == PersonState[p] IN 82 | /\ ~pState.waiting 83 | /\ pState.location \in Floor 84 | /\ \E f \in Floor : 85 | /\ f /= pState.location 86 | /\ PersonState' = [PersonState EXCEPT ![p] = [@ EXCEPT !.destination = f]] 87 | /\ UNCHANGED <> 88 | 89 | CallElevator(p) == \* Person calls the elevator to go in a certain direction from their floor 90 | LET pState == PersonState[p] IN 91 | LET call == [floor |-> pState.location, direction |-> GetDirection[pState.location, pState.destination]] IN 92 | /\ ~pState.waiting 93 | /\ pState.location /= pState.destination 94 | /\ ActiveElevatorCalls' = 95 | IF \E e \in Elevator : 96 | /\ CanServiceCall[e, call] 97 | /\ ElevatorState[e].doorsOpen 98 | THEN ActiveElevatorCalls 99 | ELSE ActiveElevatorCalls \cup {call} 100 | /\ PersonState' = [PersonState EXCEPT ![p] = [@ EXCEPT !.waiting = TRUE]] 101 | /\ UNCHANGED <> 102 | 103 | OpenElevatorDoors(e) == \* Open the elevator doors if there is a call on this floor or the button for this floor was pressed. 104 | LET eState == ElevatorState[e] IN 105 | /\ ~eState.doorsOpen 106 | /\ \/ \E call \in ActiveElevatorCalls : CanServiceCall[e, call] 107 | \/ eState.floor \in eState.buttonsPressed 108 | /\ ElevatorState' = [ElevatorState EXCEPT ![e] = [@ EXCEPT !.doorsOpen = TRUE, !.buttonsPressed = @ \ {eState.floor}]] 109 | /\ ActiveElevatorCalls' = ActiveElevatorCalls \ {[floor |-> eState.floor, direction |-> eState.direction]} 110 | /\ UNCHANGED <> 111 | 112 | EnterElevator(e) == \* All people on this floor who are waiting for the elevator and travelling the same direction enter the elevator. 113 | LET eState == ElevatorState[e] IN 114 | LET gettingOn == PeopleWaiting[eState.floor, eState.direction] IN 115 | LET destinations == {PersonState[p].destination : p \in gettingOn} IN 116 | /\ eState.doorsOpen 117 | /\ eState.direction /= "Stationary" 118 | /\ gettingOn /= {} 119 | /\ PersonState' = [p \in Person |-> 120 | IF p \in gettingOn 121 | THEN [PersonState[p] EXCEPT !.location = e] 122 | ELSE PersonState[p]] 123 | /\ ElevatorState' = [ElevatorState EXCEPT ![e] = [@ EXCEPT !.buttonsPressed = @ \cup destinations]] 124 | /\ UNCHANGED <> 125 | 126 | ExitElevator(e) == \* All people whose destination is this floor exit the elevator. 127 | LET eState == ElevatorState[e] IN 128 | LET gettingOff == {p \in Person : PersonState[p].location = e /\ PersonState[p].destination = eState.floor} IN 129 | /\ eState.doorsOpen 130 | /\ gettingOff /= {} 131 | /\ PersonState' = [p \in Person |-> 132 | IF p \in gettingOff 133 | THEN [PersonState[p] EXCEPT !.location = eState.floor, !.waiting = FALSE] 134 | ELSE PersonState[p]] 135 | /\ UNCHANGED <> 136 | 137 | CloseElevatorDoors(e) == \* Close the elevator doors once all people have entered and exited the elevator on this floor. 138 | LET eState == ElevatorState[e] IN 139 | /\ ~ENABLED EnterElevator(e) 140 | /\ ~ENABLED ExitElevator(e) 141 | /\ eState.doorsOpen 142 | /\ ElevatorState' = [ElevatorState EXCEPT ![e] = [@ EXCEPT !.doorsOpen = FALSE]] 143 | /\ UNCHANGED <> 144 | 145 | MoveElevator(e) == \* Move the elevator to the next floor unless we have to open the doors here. 146 | LET eState == ElevatorState[e] IN 147 | LET nextFloor == IF eState.direction = "Up" THEN eState.floor + 1 ELSE eState.floor - 1 IN 148 | /\ eState.direction /= "Stationary" 149 | /\ ~eState.doorsOpen 150 | /\ eState.floor \notin eState.buttonsPressed 151 | /\ \A call \in ActiveElevatorCalls : \* Can move only if other elevator servicing call 152 | /\ CanServiceCall[e, call] => 153 | /\ \E e2 \in Elevator : 154 | /\ e /= e2 155 | /\ CanServiceCall[e2, call] 156 | /\ nextFloor \in Floor 157 | /\ ElevatorState' = [ElevatorState EXCEPT ![e] = [@ EXCEPT !.floor = nextFloor]] 158 | /\ UNCHANGED <> 159 | 160 | StopElevator(e) == \* Stops the elevator if it's moved as far as it can in one direction 161 | LET eState == ElevatorState[e] IN 162 | LET nextFloor == IF eState.direction = "Up" THEN eState.floor + 1 ELSE eState.floor - 1 IN 163 | /\ ~ENABLED OpenElevatorDoors(e) 164 | /\ ~eState.doorsOpen 165 | /\ nextFloor \notin Floor 166 | /\ ElevatorState' = [ElevatorState EXCEPT ![e] = [@ EXCEPT !.direction = "Stationary"]] 167 | /\ UNCHANGED <> 168 | 169 | (***************************************************************************) 170 | (* This action chooses an elevator to service the call. The simple *) 171 | (* algorithm picks the closest elevator which is either stationary or *) 172 | (* already moving toward the call floor in the same direction as the call. *) 173 | (* The system keeps no record of assigning an elevator to service a call. *) 174 | (* It is possible no elevator is able to service a call, but we are *) 175 | (* guaranteed an elevator will eventually become available. *) 176 | (***************************************************************************) 177 | DispatchElevator(c) == 178 | LET stationary == {e \in Elevator : ElevatorState[e].direction = "Stationary"} IN 179 | LET approaching == {e \in Elevator : 180 | /\ ElevatorState[e].direction = c.direction 181 | /\ \/ ElevatorState[e].floor = c.floor 182 | \/ GetDirection[ElevatorState[e].floor, c.floor] = c.direction } IN 183 | /\ c \in ActiveElevatorCalls 184 | /\ stationary \cup approaching /= {} 185 | /\ ElevatorState' = 186 | LET closest == CHOOSE e \in stationary \cup approaching : 187 | /\ \A e2 \in stationary \cup approaching : 188 | /\ GetDistance[ElevatorState[e].floor, c.floor] <= GetDistance[ElevatorState[e2].floor, c.floor] IN 189 | IF closest \in stationary 190 | THEN [ElevatorState EXCEPT ![closest] = [@ EXCEPT !.floor = c.floor, !.direction = c.direction]] 191 | ELSE ElevatorState 192 | /\ UNCHANGED <> 193 | 194 | Init == 195 | \* Have people start at a random floor. 196 | /\ PersonState \in [Person -> [location : Floor, destination : Floor, waiting : {FALSE}]] 197 | /\ ActiveElevatorCalls = {} 198 | \* Have all elevators start at the first floor. 199 | /\ ElevatorState \in [Elevator -> [floor : {1}, direction : {"Stationary"}, doorsOpen : {FALSE}, buttonsPressed : {{}}]] 200 | 201 | Next == \* The next-state relation 202 | \/ \E p \in Person : PickNewDestination(p) 203 | \/ \E p \in Person : CallElevator(p) 204 | \/ \E e \in Elevator : OpenElevatorDoors(e) 205 | \/ \E e \in Elevator : EnterElevator(e) 206 | \/ \E e \in Elevator : ExitElevator(e) 207 | \/ \E e \in Elevator : CloseElevatorDoors(e) 208 | \/ \E e \in Elevator : MoveElevator(e) 209 | \/ \E e \in Elevator : StopElevator(e) 210 | \/ \E c \in ElevatorCall : DispatchElevator(c) 211 | 212 | TemporalAssumptions == \* Assumptions about how elevators and people will behave 213 | /\ \A p \in Person : WF_Vars(CallElevator(p)) 214 | /\ \A e \in Elevator : WF_Vars(OpenElevatorDoors(e)) 215 | /\ \A e \in Elevator : WF_Vars(EnterElevator(e)) 216 | /\ \A e \in Elevator : WF_Vars(ExitElevator(e)) 217 | /\ \A e \in Elevator : SF_Vars(CloseElevatorDoors(e)) 218 | /\ \A e \in Elevator : SF_Vars(MoveElevator(e)) 219 | /\ \A e \in Elevator : WF_Vars(StopElevator(e)) 220 | /\ \A c \in ElevatorCall : SF_Vars(DispatchElevator(c)) 221 | 222 | Spec == 223 | /\ Init 224 | /\ [][Next]_Vars 225 | \* /\ TemporalAssumptions 226 | 227 | THEOREM Spec => [](TypeInvariant /\ SafetyInvariant /\ TemporalInvariant) 228 | 229 | ==================================================================================================== 230 | \* Modification History 231 | \* Last modified Wed Mar 28 20:46:03 EDT 2018 by williamschultz 232 | \* Created Fri Mar 23 00:50:27 EDT 2018 by williamschultz 233 | 234 | \* Modification History 235 | \* Created Sun Mar 25 12:32:39 EDT 2018 by williamschultz 236 | -------------------------------------------------------------------------------- /new_examples/Elevator/ElevatorAnimated.tla: -------------------------------------------------------------------------------- 1 | ------------------------------------- MODULE ElevatorAnimated ------------------------------------- 2 | 3 | (**************************************************************************************************) 4 | (* Animation and View Definitions for Elevator system *) 5 | (**************************************************************************************************) 6 | 7 | EXTENDS Elevator, SVG 8 | 9 | 10 | (* View helpers. *) 11 | Injective(f) == \A x, y \in DOMAIN f : f[x] = f[y] => x = y 12 | 13 | \* Establish a fixed mapping to assign an ordering to elements in these sets. 14 | PersonId == CHOOSE f \in [Person -> 1..Cardinality(Person)] : Injective(f) 15 | ElevatorId == CHOOSE f \in [Elevator -> 1..Cardinality(Elevator)] : Injective(f) 16 | 17 | \* Dimensions of an elevator. 18 | ElevatorDims == [width |-> 35, height |-> 50] 19 | FloorHeight == ElevatorDims.height + 10 20 | FloorYPos(f) == f * FloorHeight 21 | 22 | \* Gives the (x,y) base position of an elevator. 23 | ElevatorPos(e) == [x |-> (150 + ElevatorId[e] * (ElevatorDims.width + 3)), y |-> FloorYPos(ElevatorState[e].floor)] 24 | 25 | (**************************************************************************************************) 26 | (* ELEVATOR elements. *) 27 | (**************************************************************************************************) 28 | ElevatorElem(e) == 29 | LET pos == ElevatorPos(e) 30 | dims == ElevatorDims 31 | color == IF ElevatorState[e].doorsOpen THEN "green" ELSE "black" IN 32 | Rect(pos.x, pos.y, dims.width, dims.height, [fill |-> color]) 33 | 34 | \* Elements that show which direction an elevator is moving. 35 | ElevatorDirElem(e) == 36 | LET pos == ElevatorPos(e) 37 | dims == ElevatorDims 38 | mid == pos.y + 17 39 | yPos == IF ElevatorState[e].direction = "Down" 40 | THEN mid - 17 41 | ELSE IF ElevatorState[e].direction = "Up" THEN mid + 17 42 | ELSE mid IN 43 | Rect(pos.x + 1, yPos, dims.width-2, 2, [fill |-> "white"]) 44 | 45 | ElevatorDirElems == {ElevatorDirElem(e) : e \in Elevator} 46 | ElevatorElems == {ElevatorElem(e) : e \in Elevator} 47 | 48 | (**************************************************************************************************) 49 | (* PERSON Elements. *) 50 | (**************************************************************************************************) 51 | 52 | PersonRadius == 3 53 | PersonXPosBase == 30 54 | PersonXPos(xBase, p) == xBase + (PersonId[p] * 9) 55 | PersonYPos(p) == FloorYPos(PersonState[p].location) + 10 56 | 57 | \* Person who is currently on a floor, not in an elevator. 58 | FloorPersonElem(p) == 59 | LET person == PersonState[p] 60 | pos == [y |-> PersonYPos(p), x |-> PersonXPos(PersonXPosBase, p)] 61 | color == IF person.waiting THEN "darkred" ELSE "blue" IN 62 | Circle(pos.x, pos.y, PersonRadius, [fill |-> color]) 63 | 64 | \* Person who is currently in an elevator. 65 | ElevatorPersonElem(p) == 66 | LET person == PersonState[p] 67 | elevPos == ElevatorPos(person.location) 68 | pos == [elevPos EXCEPT !.x = PersonXPos(elevPos.x, p), !.y = @ + 10] IN 69 | Circle(pos.x, pos.y, PersonRadius, [fill |-> "gray"]) 70 | 71 | PersonElem(p) == 72 | \* A person should always be waiting or in an elevator. 73 | LET person == PersonState[p] IN 74 | CASE person.location \in Floor -> FloorPersonElem(p) 75 | [] person.location \in Elevator -> ElevatorPersonElem(p) 76 | 77 | PersonDestinationElem(p) == 78 | LET person == PersonState[p] IN 79 | CASE person.location \in Floor -> 80 | LET xPos == PersonXPos(PersonXPosBase, p) 81 | dims == (IF (person.destination > person.location) 82 | THEN [height |-> (FloorYPos(person.destination) - PersonYPos(p)), 83 | yPos |-> PersonYPos(p)] 84 | ELSE [height |-> (PersonYPos(p) - FloorYPos(person.destination)), 85 | yPos |-> FloorYPos(person.destination)]) IN 86 | Rect(xPos, dims.yPos, 1, dims.height, [fill |-> "lightgray"]) 87 | [] person.location \in Elevator -> 88 | LET elevator == ElevatorState[person.location] 89 | elevPos == ElevatorPos(person.location) 90 | xPos == PersonXPos(elevPos.x, p) 91 | personYPos == elevPos.y + 10 92 | dims == (IF (person.destination > elevator.floor) 93 | THEN [height |-> (FloorYPos(person.destination) - personYPos), 94 | yPos |-> personYPos] 95 | ELSE [height |-> (personYPos - FloorYPos(person.destination)), 96 | yPos |-> FloorYPos(person.destination)]) IN 97 | Rect(xPos, dims.yPos, 1, dims.height, [fill |-> "lightgray"]) 98 | 99 | PersonDestinationElems == {PersonDestinationElem(p) : p \in Person} 100 | PeopleTitle == Text(PersonXPosBase, 50, "People", <<>>) 101 | PersonElems == {PersonElem(p) : p \in Person} \cup PersonDestinationElems 102 | 103 | (**************************************************************************************************) 104 | (* ELEVATOR CALL elements. *) 105 | (**************************************************************************************************) 106 | IsFloorCall(floor, dir) == \E c \in ActiveElevatorCalls : c.floor = floor /\ c.direction = dir 107 | 108 | ButtonXPos == 90 109 | Button(floor, dir) == 110 | LET x == ButtonXPos 111 | y == FloorYPos(floor) + (IF dir = "Up" THEN 25 ELSE 16) IN 112 | Rect(x, y, 7, 7, [fill |-> IF IsFloorCall(floor, dir) THEN "orange" ELSE "black"]) 113 | 114 | ElevatorButtonElem(floor) == 115 | LET upButton == Button(floor, "Up") 116 | downButton == Button(floor, "Down") IN 117 | Group(<>, <<>>) 118 | 119 | ElevatorButtonElems == {ElevatorButtonElem(f) : f \in Floor} 120 | 121 | (**************************************************************************************************) 122 | (* FLOOR elements. *) 123 | (**************************************************************************************************) 124 | 125 | FloorSeparator(floor) == Rect(5, FloorYPos(floor), 350, 1, [fill |-> "lightgray"]) 126 | FloorSeparators == {FloorSeparator(f) : f \in Floor} 127 | 128 | FloorLabel(floor) == Text(10, FloorYPos(floor)+15, ToString(floor), <<>>) 129 | FloorLabels == {FloorLabel(f) : f \in Floor} 130 | 131 | AllElems == SetToSeq(ElevatorElems) \o 132 | SetToSeq(FloorSeparators) \o 133 | SetToSeq(FloorLabels) \o 134 | SetToSeq(ElevatorDirElems) \o 135 | SetToSeq(PersonElems) \o 136 | <> \o 137 | SetToSeq(ElevatorButtonElems) 138 | 139 | View == SVGElemToString(Group(AllElems, ("transform" :> "translate(10 10)"))) 140 | 141 | ----------------------------------------------------------------- 142 | Maximum(S) == CHOOSE x \in S : \A y \in S : x >= y 143 | 144 | \* ReducedInit == 145 | \* \* Have people start at a random even floor. Reduce the number of initial states by limiting initial destinations. 146 | \* /\ PersonState \in [Person -> [location : {x \in Floor : x % 2 = 0 }, 147 | \* destination : {1, Maximum(Floor)}, 148 | \* waiting : {FALSE}]] 149 | \* /\ ActiveElevatorCalls = {} 150 | \* \* Have all elevators start at the first floor. 151 | \* /\ ElevatorState \in [Elevator -> [floor : {1}, direction : {"Stationary"}, doorsOpen : {FALSE}, buttonsPressed : {{}}]] 152 | 153 | \* AnnotatedNext == \* The animation next-state relation. 154 | \* \/ \E p \in Person : PickNewDestination(p) /\ ActionName("PickNewDestination") 155 | \* \/ \E p \in Person : CallElevator(p) /\ ActionName("CallNewElevator") 156 | \* \/ \E e \in Elevator : OpenElevatorDoors(e) /\ ActionName("OpenElevatorDoors") 157 | \* \/ \E e \in Elevator : EnterElevator(e) /\ ActionName("EnterElevator") 158 | \* \/ \E e \in Elevator : ExitElevator(e) /\ ActionName("ExitElevator") 159 | \* \/ \E e \in Elevator : CloseElevatorDoors(e) /\ ActionName("CloseElevatorDoors") 160 | \* \/ \E e \in Elevator : MoveElevator(e) /\ ActionName("MoveElevator") 161 | \* \/ \E e \in Elevator : StopElevator(e) /\ ActionName("StopElevator") 162 | \* \/ \E c \in ElevatorCall : DispatchElevator(c) /\ ActionName("DispatchElevator") 163 | 164 | \* AnimSpec == 165 | \* /\ AnimatedInit(ReducedInit, View) 166 | \* /\ [][AnimatedNext(AnnotatedNext, View, TRUE)]_<> 167 | 168 | ==================================================================================================== 169 | \* Modification History 170 | \* Last modified Sat Jul 07 16:26:03 EDT 2018 by williamschultz 171 | \* Created Fri Mar 23 00:50:27 EDT 2018 by williamschultz 172 | -------------------------------------------------------------------------------- /new_examples/Elevator/MCElevatorAnimated.cfg: -------------------------------------------------------------------------------- 1 | SPECIFICATION Spec 2 | CONSTANT Person = {p0, p1, p2} 3 | CONSTANT Elevator = {e0, e1} 4 | CONSTANT FloorCount = 4 5 | INVARIANT AllInElevator -------------------------------------------------------------------------------- /new_examples/Elevator/MCElevatorAnimated.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE MCElevatorAnimated ---- 2 | EXTENDS TLC, ElevatorAnimated, FiniteSets 3 | 4 | NotInit == ~Init 5 | AllInElevator == ~(\A p \in Person : PersonState[p].location \in Elevator) 6 | 7 | ==== -------------------------------------------------------------------------------- /new_examples/Elevator/README.md: -------------------------------------------------------------------------------- 1 | ## Elevator TLA+ Animation Demo 2 | 3 | To generate a trace with TLC and produce an animation for the `Elevator.tla` spec in this directory, run the following commands: 4 | 5 | ```bash 6 | # Download latest TLC tools and CommunityModules JAR. 7 | ./get_tlatools.sh 8 | 9 | # Model check the spec under simulation mode and produce an error trace. 10 | ./model_check.sh MCElevatorAnimated 11 | 12 | # Run trace exploration to generate animated trace and generate HTML output. 13 | ./trace_explore.sh MCElevatorAnimated 14 | ``` 15 | 16 | The visualization itself is defined in `ElevatorAnimated.tla`. You can go in there and tweak the layout of the visualization and re-run trace exploration step to produce a new animation HTML, which should be saved into the `animation.html` file, which you can open in any browser. 17 | 18 | If you have run model checking once, the TLC output will be saved in the `MCElevatorAnimated.out` file, which is the input fed into the trace explorer. So, once you run model checking and produce an output file, you can continue producing animations of that trace without re-running the model checker. If you want to animate a different trace, then you will need to run the model checker again followed by the trace explorer. 19 | -------------------------------------------------------------------------------- /new_examples/Elevator/aliases.sh: -------------------------------------------------------------------------------- 1 | alias tlc="java -cp tla2tools.jar:CommunityModules-202002121931.jar tlc2.TLC" 2 | alias trace="java -cp tla2tools.jar:CommunityModules-202002121931.jar tlc2.TraceExplorer" -------------------------------------------------------------------------------- /new_examples/Elevator/get_tlatools.sh: -------------------------------------------------------------------------------- 1 | # Download nightly TLA+ tools JAR and Community Modules. 2 | wget https://nightly.tlapl.us/dist/tla2tools.jar 3 | wget https://github.com/tlaplus/CommunityModules/releases/download/202002121931/CommunityModules-202002121931.jar -------------------------------------------------------------------------------- /new_examples/Elevator/model_check.sh: -------------------------------------------------------------------------------- 1 | # 2 | # Run TLC to produce error trace and then run trace explorer on the trace. 3 | # 4 | 5 | source aliases.sh 6 | 7 | SPEC=$1 8 | 9 | # Run the TLC model checker to produce an error trace and save the output. 10 | echo "Running model checking to produce error trace." 11 | tlc -simulate -depth 85 -tool $SPEC | tee $SPEC.out -------------------------------------------------------------------------------- /new_examples/Elevator/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TLA+ Trace Animator 5 | 6 | 7 | 8 | 9 | 10 | 18 | 19 |
20 | 21 |

TLA+ Trace Animator

22 | 23 | 24 | 27 | 28 | 29 |
30 | 33 |
34 | 35 | 36 | 37 | 38 |
39 | 40 |
41 | 42 | 43 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /new_examples/Elevator/traceExp.txt: -------------------------------------------------------------------------------- 1 | View -------------------------------------------------------------------------------- /new_examples/Elevator/trace_explore.sh: -------------------------------------------------------------------------------- 1 | # 2 | # Run TLC to produce error trace and then run trace explorer on the trace. 3 | # 4 | 5 | source aliases.sh 6 | 7 | SPEC=$1 8 | 9 | # Generate a trace with specified trace expressions evaluated. 10 | echo "Running trace explorer." 11 | trace -traceExpressions -expressionsFile=traceExp.txt -overwrite $SPEC 12 | 13 | # Filter for the trace expression we evaluated. 14 | grep "traceExpression" TE.out | sed -E "s/.*_traceExpression_1 = //" | sed -E "s/\"//" > viewExps.txt 15 | 16 | # This is a crude way to replace a template string with the SVG output, but it works. We basically take everything 17 | # in the template file before the magic string ('@SVG_TEXT@'), add it to the output file, then append the animation SVG elements, 18 | # and then append everything in the template file after the magic string. Can probably also do this with 'sed' but this works for now. 19 | animfile="animation.html" 20 | grep -B 10000 "@SVG_TEXT@" template.html > $animfile 21 | cat viewExps.txt >> $animfile 22 | grep -A 10000 "@SVG_TEXT@" template.html >> $animfile 23 | --------------------------------------------------------------------------------