Tag(sp22)
#lang forge/bsl "lecture_example" "tim_nelson@brown.edu"
).dev
branch of Forge has a number of fixes (including evaluator disabled on old instances, some better errors in example blocks).one
. It's very convenient but dangerous. If you write one a1, a2: Animal | ...
but the ...
is symmetric (that is, it doesn't care about which is a1
and which is a2
), then the entire constraint can't be satisfied except under very limited conditions–-the assignment to the variables is not unique!When we're talking about whether or not a reachable state violates a desirable property (recall we sometimes say that if this holds, is an invariant of the system), it's useful to think geometrically. Here's a picture of the space of all states, with the cluster of "good" states separated from the "bad":
If this space is large, we probably can't use trace-finding to get a real proof: we'd have to either reduce the trace length (in which case, maybe there's a bad state just barely out of reach of that length) or we'd be waiting until the sun expands and engulfs the earth.
Traces are still useful, especially for finding shallow bugs, and the technique is used in industry! But we need more than one tool in our bag of tricks.
Let's break the problem down. What if we just consider reachability for traces of length –-that is, traces of only one state, an initial
state?
This we can check in Forge just by asking for a state s
satisfying {initial[s] and wellformed[s] and not P[s]}.
There's no exponential blowup with trace length since the transition predicates are never even involved! If we see something like this:
We know that at least the starting states are good. If instead there was a region of the starting states that overlapped the bad states, then we immediately know that the property isn't invariant.
We can surely also check whether there are bad states within transition. We're using the transition predicate (or predicates) now, but only once. Forge can also do this; we ask for a pair of states s0
, s1
satisfying {initial[s0] and someTransition[s0, s1] and not P[s1]}
(where someTransition
is my shorthand for allowing any transition predicate to work; we could write the predicate ourselves).
If Forge doesn't find any way for the second state to violate , it means we have a picture like this:
It's looking promising! Note that in general, there might be overlap (as shown) between the set of possible initial states and the set of possible second states. (For example, imagine if we allowed a doNothing
transition at any time).
If we keep following this process exactly, we'll arrive back at the trace-based approach: a set of 3rd states, a set of 4th states, and so on.
Sometimes great ideas arise from dire limitations. What if we limit ourselves to only ever asking Forge for these two state examples? That would solve the exponential-blowup problem of traces, but how can we ever get something useful, that is, a result that isn't limited to trace length 1?
I claim that we can use these small, efficient queries to often show that holds at any finite length from a starting state. But how?
By no longer caring whether the pre-state of the check is reachable or not.
We'll ask Forge whether {P[s0] and someTransition[s0, s1] and not P[s1]}
is satisfiable for any pair of states. Just so long as the pre-state satisfies and the post-state doesn't. We're asking Forge if it can find a transition that looks like this:
If the answer is no, then it is simply impossible for any transition predicate to stop property from holding: if it holds in the pre-state, it must hold in the post-state. Always.
But if that's true, and we know that all initial states satisfy , then all states reachable in transition satisfy (by what we just checked). And if that's true, then all states reachable in transitions satisfy also (since all potential pre-states must satisfy ). And so on: any state that's reachable in a finite number of transitions must satisfy .
If you've seen "proof by induction" before in another class, we've just applied the same idea here. Except, rather than using it to show that the sum of the numbers from to is , we've just used it to prove that is invariant in our system.
In Tic-Tac-Toe, this would be something like "cheating states can't be reached with legal moves". In an operating system, this might be "two processes can never modify a block of memory simultaneously". In hardware, it might be "only one device has access to write to the bus at any point".
For most computer-scientists, I think that this feels like a far more relatable and immediately useful example of the induction principle. That's not to dismiss mathematical induction! I quite like it (and it's useful for establishing some useful results related to Forge). But multiple perspectives enrich life.
What if Forge does find a transition like this? Does it mean that is not an invariant of the system?
This technique is a great way to quickly show that is invariant, but if it fails, we need to do more work.
The solution to this problem is called "enriching the invariant". More on this next week.
As written, the second test will take a very long time (around 3 minutes on my laptop). Why? Is there some sort of hint we could provide helpfully to Forge?
I forgot to allow doNothing
in the above code! But it doesn't matter. I made a mistake in lecture: the pre-state I gave is something like:
But doNothing
can't change it! So this property is indeed inductive, and the mistake was mine. (Really, I ought to have run it in Forge, rather than my imagination!)
What would it mean for this verification idea if there were simply no initial states, or no way to take a certain transition? That would probably be a bug in the model; how would it impact our proof?
Look again at the two checks we wrote. If initial
were unsatisfiable by any state, surely the Step 1 check would also be unsatisfiable (since it just adds more constraints). Similarly, unsatisfiable transition predicates would limit the power of Step 2 to find ways that the system could transition out of safety. This would mean that our confidence in the check was premature: Forge would find no initial bad states, but only because we narrowed its search to nothing!
This problem is called vacuity, and I'll give you another example. Suppose I told you: "All my billionaire friends love Logic for Systems". I have, as far as I know anyway, no billionaire friends. So is the sentence true or false? If you asked Forge, it would say that the sentence was true–-after all, there's no billionaire friend of mine who doesn't love Logic for Systems…
This is a problem you might hear about in other courses like 0220, or in the philosophy department. And so there's a risk you'll think vacuity is silly, or possibly a topic for debate among people who like drawing their As upside down and their Es backwards, and love writing formulas with lots of Greek letters in. Don't be fooled! Vacuity is a major problem even in industrial settings like Intel, because verification tools are literal-minded. (Still doubtful? Ask Tim to send you links.)
At the very least, we'd better test that wellformed
can be satisfied:
This isn't a guarantee of trustworthiness, but it's a start. And it's easy to check with Forge.