Transition Language
All four operations (init!
, transition!
, readonly!
and property!
) use
the transition language (although property!
operations are not technically transitions).
The operations can be declared like so:
transition!{
name(params...) {
TransitionStmt
}
}
The TransitionStmt
language allows very simple control-flow with if-else and match
statements, and allows let-bindings.
TransitionStmt :=
| TransitionStmt; TransitionStmt;
| if $expr { TransitionStmt }
| if $expr { TransitionStmt } else { TransitionStmt }
| match $expr { ( $pat => { TransitionStmt } )* }
| let $pat = $expr;
| SingleStmt
There are four fundamental statements: init
, update
, require
, and assert
.
For convenience, there are also higher-level statements that can be expressed
in those terms.
SingleStmt :=
| init $field_name = $expr;
| update $field_name = $expr;
| require $bool_expr;
| assert $bool_expr (by { ... })? ;
// Higher-level statements:
| require let $pat = $expr;
| assert let $pat = $expr;
| update $field_name ([sub_idx] | .subfield)* = $expr;
| remove ... | have ... | add ... | deposit ... | guard ... | withdraw ...
Each $field_name
should be a valid field name as declared in the fields
block of the state machine definition.
Each $expr
/$bool_expr
is an ordinary Verus (spec-mode) expression.
These expressions can reference the params or bound variables.
They can also reference the pre-state of the transition, pre
(except in init!
operations).
(Note that it is not possible to access post
directly,
though this may be added in the future.)
Core statements
The init
statement
The init
statement is used only in init!
operations.
Each field of the state must be initialized exactly once in each init!
operation.
If there is any control-flow-branching, then each field must be initialized
exactly once in each branch.
Example:
state_machine!{ InitExample {
fields {
pub x: int,
pub y: int,
}
// Okay.
init!{
initialize_1(x_init_value: int) {
init x = x_init_value;
init y = 0;
}
}
// Not okay (y is not initialized)
init!{
initialize_2(x_init_value: int) {
init x = x_init_value;
}
}
// Not okay (y is not initialized in the second branch)
init!{
initialize_3(b: bool) {
if b {
init x = 0;
init y = 1;
} else {
init x = 5;
}
}
}
}}
The update
statement
The update
statement is the counterpart of init
for the transition!
operations:
it sets the value of a field for a post
state.
However, (unlike init
), not every field needs to be updated.
Any field for which no update
statement appears will implicitly maintain its value
from the pre
state.
Example:
state_machine!{ TransitionExample {
fields {
pub x: int,
pub y: int,
}
// Okay.
transition!{
transition_1(x_new_value: int) {
update x = x_new_value;
}
}
// Okay.
transition!{
transition_2(b: bool) {
if b {
update x = 0;
update y = 1;
} else {
update x = 5;
}
}
}
// Not okay.
// (The first 'update' is meaningless because it is immediately overwritten.)
transition!{
transition_3(b: bool) {
update x = 0;
update x = 1;
}
}
}}
The require
statement
The require
statement adds an enabling condition to the operation.
This is a condition that must hold in order for the operation to be performed.
A require
can be used in any of the operation types.
The assert
statement
The assert
statement declares a safety condition on the transition. Verus creates a proof obilgation for the validity of the state machine: the assert
must follow from the state invariants, and from any enabling conditions (require
statements) given prior to the assert
.
If the user needs to provide manual proofs, they can do so using an assert-by:
assert $bool_expr by {
/* proof here */
};
(TODO need a better example here)
Because we demand that the assert
statement is proved,
clients of the state machine may assume that the condition holds whenever the transition is enabled (i.e., all its require
conditions hold).
other proofs can assume that the predicate holds whenever this transition is enabled.
Therefore, the assert
statement is not itself an enabling condition; that is, clients do not need to show the condition holds in order to show that the operation is enabled.
High-level statements
require let
and assert let
You can write,
require let $pat = $expr;
where $pat
is a refutable pattern.
This is roughly equivalent to writing the following (which would ordinarily be disallowed
because of the refutable pattern).
require ($expr matches $pat);
let $pat = $expr;
The assert let
statement is similar, but for assert
instead of require
.
Example:
state_machine!{ RequireLetExample {
fields {
x_opt: Option<int>,
}
transition!{
add_1() {
require let Some(x) = pre.x_opt;
update x_opt = Some(x + 1);
}
}
// Equivalent to:
transition!{
add_1_alternative() {
require pre.x_opt.is_Some();
let x = pre.x_opt.get_Some_0();
update x_opt = Some(x + 1);
}
}
}}
Update with subscript and fields
Note: This feature is currently in-development.
To update nested data structures, you can use fields (for structs)
or subscripts (for maps or sequences) in update
statements.
For example,
update field.x.y[idx].z[key] = expr;
remove
/ have
/ add
/ deposit
/ guard
/ withdraw
These are complex operations used specially for specific sharding strategies
in a tokenized_state_machine
.
See the strategy reference for details.