Nondeterminism¶
Nondeterminism¶
So far all of our specs have been deterministic. While there are multiple possible starting states, the behavior from each starting state is fixed. Most systems aren’t deterministic. Some examples:
Randomness creates a new possible behavior for each possible random value.
Interactive systems aren’t deterministic, since we don’t know what the user is going to do and in what order they do it.
We might know the range of readings a sensor could get, but not what specific reading it will get.
If the system has many moving independent parts, we don’t know in which order they’ll run.
The last case, concurrency, we’ll cover in the next chapter. To handle the rest, PlusCal has a couple of new constructs.
with¶
We’ve already seen with used to create local variables:
with tmpx = x, tmpy = y do
y := tmpx;
x := tmpy;
end with;
Remember how, when defining the spec variables, we can write x \in set
instead of x = val
? We can do the same here, too.
with roll \in 1..6 do
sum := sum + roll;
end with;
Inside this block, roll
can be any one of those six values. The model checker will try all six, effectively creating six new behaviors from that single statement.
Tip
You can combine deterministic and nondeterministic assignments in a single with
statement. The following is valid:
with
x \in BOOLEAN,
y \in 1..10,
z = TRUE
do
\* ...
end with;
You can also nondeterministically pull from a variable set:
with thread \in sleeping do
sleeping := sleeping \ {thread};
end with;
Note
If the variable set is empty, the with
will block, which can lead to a deadlock. We’ll talk about deadlocks more in the next chapter, when we cover concurrency.
either-or¶
either
is nondeterministic control flow.
either
approve_pull_request();
or
request_changes();
or
reject_request();
end either;
On evaluating this, TLC will create three branches:
You can have labels inside an either statement. Either statements are especially useful for implementing state machines.
Using Nondeterminism¶
Nondeterminism is the first major break between specifications and programming languages. It’s also the first thing that significantly raises our level of abstraction. In particular, it lets us abstract over “sad paths” in our program.
Say we’re specifying a large business workflow, and as one step in it, an employee requests access to a resource. In the happy path, she makes the request, it’s assigned to her, and the workflow continues. There are many business reasons the assignment might be rejected:
The employee isn’t authorized to use the resource
The resource is in use and cannot be reassigned until it’s free
The resource can only be assigned if a CI check passes, and it failed
The request was rejected by a higher-up
The code to reassign the resource had a bug and crashed
To fully represent all of these possible error states, we’d have to include a lot of information in our specification: the authorization policies, reserve policies, the CI process, checkout code, etc. Not to mention all of the other possible errors! This is a lot of work, and if the resource checkout is only a small part of our workflow, then it’s a lot of work that could have been spent on studying the bigger picture.
This is where nondeterminism is really useful. We don’t need to put in the details for all those errors. We only need to say the assignment succeeds, or there’s an error:
macro request_resource(r) begin
either
reserved := reserved \union {r};
or
\* Request failed
skip;
end either;
end macro;
If we need to also model the type of error (if that affects our recovery logic), we can represent exactly as many as we care about:
macro request_resource(r) begin
either
reserved := reserved \union {r};
failure_reason := "";
or
with reason \in {"unauthorized", "in_use", "other"} do
failure_reason := reason;
end with;
or
\* some other error
skip;
end either;
end macro;
either or skip
is a common nondeterminism pattern and it’s quite useful in a lot of places.
We can also use nondeterminism to represent outside actions. If we’re modeling requests that are coming into a system, we don’t need to pick a specific request to spec. Instead we can define a RequestType
and pull from that on every inbound request.
RequestType == [from: Client, type: {"GET", "POST"}, params: ParamType]
with request \in RequestType do
if request.type = "GET" then
\* get logic
elsif request.type = "POST" then
\* post logic
else
\* something's wrong with our spec!
assert FALSE;
end if;
end with;
Example: A Calculator¶
One way we use nondeterminism is to simulate user input. Our system has to handle all user actions properly, so we model them as nondeterministically taking actions from a valid set. As an example, let’s specify an extremely simple calculator. While TLA+ can’t represent decimal numbers, we can do addition, multiplication, and subtraction. First, let’s allow users to add any digit to a current sum:
---- MODULE calculator ----
EXTENDS Integers, TLC
CONSTANT NumInputs
Digits == 0..9
(* --algorithm calculator
variables
i = 0;
sum = 0;
begin
Calculator:
while i < NumInputs do
with x \in Digits do
\* Add
sum := sum + x;
end with;
i := i + 1;
end while;
end algorithm; *)
====
Two things to notice:
The user can input any single digit, which is represented with a
with
. We restrict their options to0..9
to keep the state space smaller.We restrict the spec to only
NumInputs
operations per behavior. If we instead didwhile TRUE
,sum
would be unbounded, the state space would be infinitely large, and the model checker would run forever. I do makeNumInputs
a Model Constants for easy modification.
For all this spec, I’m using NumInputs <- 5
, for 1043 states / 187 distinct. This is a much higher ratio of (seen states / distinct states) than we’ve seen before: adding 5 and then 3 gives the same state as adding 3 and then 5. There’s also a much higher ratio of (seen states / initial states). Before, we only had one behavior from each starting state, but now we have many.
To allow users to also subtract and multiply, we can place the addition logic in an either
branch, and then create two more branches.
Calculator:
while i < NumInputs do
with x \in Digits do
+ either
\* Add
sum := sum + x;
+ or
+ \* Subtract
+ sum := sum - x;
+ or
+ \* Multiply
+ sum := sum * x;
+ end either;
end with;
i := i + 1;
end while;
Allowing a nondeterministic choice of operator bloats the state space further 71333 states / 16551 distinct. When dealing with nondeterminism, there are lots of possible states, which is one reason it’s harder to reason about.
Normally we’d write an invariant to test that this spec is working correctly, but aside from a type invariant or two there’s not much to check here. So let’s instead turn things around and see if we can use TLA+ to find a solution to something. Can we reach some number, say 417
, in five inputs?
To find this, let’s add an invariant saying sum
is not 417. Then, if sum
is reachable, the model checker will fail, and give us an error trace representing a path to 417.
---- MODULE calculator ----
EXTENDS Integers, TLC
-CONSTANT NumInputs
+CONSTANT NumInputs, Target
Digits == 0..9
@@ -8,6 +8,9 @@
variables
i = 0;
sum = 0;
+define
+ Invariant == sum # Target
+end define;
begin
Calculator:
Now running the checker with INVARIANT Invariant
and NumInputs <- 5, Target <- 417
, we get this error trace:
State 1:
/\ sum = 0
/\ i = 0
State 2:
/\ sum = 1
/\ i = 1
State 3:
/\ sum = 10
/\ i = 2
State 4:
/\ sum = 60
/\ i = 3
State 5:
/\ sum = 420
/\ i = 4
State 6:
/\ sum = 417
/\ i = 5
So 417 is ((((0+1)+9)*6)*7)-3.
(This spec got me curious: what’s the smallest number we can’t reach in 5 inputs? There’s no easy way to do this as a single model-check. Instead I wrote a script to run the model checker from the command line with every value of Target
from 0 to 1000. The first number that doesn’t produce an error trace is 851.)
Summary¶
Nondeterminism is when the spec can do one of several different things in one step.
with x \in set
nondeterministically chooses a value fromset
forx
.either branch1 or branch2
nondeterministically chooses a branch to execute.Nondeterminism can be used to abstract implementation details into a high-level step.