Guard Clauses And Enabling Conditions
Before you do any serious modeling in any formal system, you need to understand the concept of guard clauses and enabling conditions.
Guard clauses or enabling conditions are the predicates that tell whether an action/transition is allowed or not. Guard clauses are critical to test deadlocks and liveness properties.
Some formal methods languages like Event-B, PRISM etc use standalone guard clauses or preconditions. FizzBee follows languages like TLA+ use embedded guard clauses where the guard is part of the action. But unlike TLA+, FizzBee infers the enabled status of the action using a simple but unconventional set of rules.
|
|
When you run the model checker, it will show a deadlock error. Because, once the switch is turned ON, then no other action is possible.
Obviously, two correct ways to fix this, depending on the requirements or the expected behavior.
- Remove the guard clause from the
On
action. This would make, the ON action idempotent. That’s like a elevator call button. You can press it multiple times, but it will not change the state.
|
|
- Allow other actions like
Off
, that will change the state back to OFF.
|
|
Instead of using if-else, you can also use require
statements.
|
|
Although if-else structure and the require statement are similar, they have some subtle differences, that we will go over later in this doc.
In an ideal world, you probably don’t need to know this. But a brief understanding of the implementation details will help you spec the model effectively and help simplify the code and speed up model checking.
FizzBee has multiple types of statements, similar to Python.
One of the types is the simple_python_stmt
, that corresponds to a subset of Python’s simple_stmt
in this grammar.
Specifically,
- assignments (a = 1, x+=2 etc)
- star_expressions (my_list.append(1) etc)
- pass
- del (at present Python’s
del
stmt is not supported, but will be added soon) Other statements likeif-elif-else
,for
,any
,while
,return
,continue
etc are notsimple_python_stmt
.
There is one exception for the method calls. If the method call is to builtin functions or functions in
the standard library, it is treated as simple_python_stmt
.
If it is a user-defined function or a fizz function, even though it is a syntactically same as
the builtin function, it is not treated as simple_python_stmt
. Let us call it, fizz_call_stmt
.
In many rules, this exception will be a bit annoying to learn, but you’ll get it once you see a lot of examples.
In the atomic context, this is how the enabled status is inferred.
-
Every action starts out as disabled
action.enabled=False
. -
For each statement that is executed, if the statement is a
2a.
simple_python_stmt
, enable the actionaction.enabled=True
.2b.
fizz_call_stmt
and the returned value is assigned, then enable the actionaction.enabled=True
.2c. if
require
fails, disable the actionaction.enabled=False
, and exits the action.2d. for any other statements, leave the enabled bit unchanged.
-
At the end of the action, if
action.enabled == True
, the action will be added to the state graph, and check for invariants etc.
The rules are simple and allows for an incredibly concise and expressive language.
|
|
If the switch is in OFF state, the ON action will be enabled only,
when the switch = "ON"
gets executed.
If the switch is in ON state already, the action starts out disabled.
And since the switch = "ON"
is not executed, the action will remain disabled.
That is, the above code is equivalent to the following code.
|
|
Similarly, when you have if,elif,else, the action will be enabled only if at least one simple statement is executed.
|
|
Of course, if instead of return
if you use any statement, it will enable the action.
|
|
Although the rule is simple, some seemingly equivalent python code will not work as expected.
For example: in this example, the On action will be enabled even if it is already ON.
|
|
The reason is, the is_on
is a simple_python_stmt
and it enables the action when executed.
This is a bit counter-intuitive, but it is a design choice to keep the language simple and expressive.
So, one way to still disable the action is to use require
statement
|
|
In this case, when the switch is already ON, when it executes the first statement,
the action will be tentatively enabled, but the require
statement will disable it.
And since it aborts the action, the action remains disabled.
This equally applies to for
, any
, while
, function calls etc. If within a for
loop,
if there is at least one simple_python_stmt
executed, the action will be enabled.
If the code calls a fizz function, and within that function there is a simple_python_stmt
executed,
the action will be enabled.