8. Statements (grammar) {#sec-statements}
Many of Dafny's statements are similar to those in traditional programming languages, but a number of them are significantly different. Dafny's various kinds of statements are described in subsequent sections.
Statements have zero or more labels and end with either a semicolon (;
) or a closing curly brace ('}').
8.1. Labeled Statement (grammar) {#sec-labeled-statement}
Examples:
class A { var f: int }
method m(a: A) {
label x:
while true {
if (*) { break x; }
}
a.f := 0;
label y:
a.f := 1;
assert old@y(a.f) == 1;
}
A labeled statement is just
- the keyword
label
- followed by an identifier, which is the label,
- followed by a colon
- and a statement.
The label may be
referenced in a break
or continue
statement within the labeled statement
(see Section 8.14). That is, the break or continue that
mentions the label must be enclosed in the labeled statement.
The label may also be used in an old
expression (Section 9.22). In this case, the label
must have been encountered during the control flow en route to the old
expression. We say in this case that the (program point of the) label dominates
the (program point of the) use of the label.
Similarly, labels are used to indicate previous states in calls of two-state predicates,
fresh expressions, unchanged expressions,
and allocated expressions.
A statement can be given several labels. It makes no difference which of these labels is used to reference the statement---they are synonyms of each other. The labels must be distinct from each other, and are not allowed to be the same as any previous enclosing or dominating label.
8.2. Block Statement (grammar) {#sec-block-statement}
Examples:
{
print 0;
var x := 0;
}
A block statement is a sequence of zero or more statements enclosed by curly braces. Local variables declared in the block end their scope at the end of the block.
8.3. Return Statement (grammar) {#sec-return-statement}
Examples:
method m(i: int) returns (r: int) {
return i+1;
}
method n(i: int) returns (r: int, q: int) {
return i+1, i + 2;
}
method p() returns (i: int) {
i := 1;
return;
}
method q() {
return;
}
A return statement can only be used in a method. It is used to terminate the execution of the method.
To return a value from a method, the value is assigned to one of the named out-parameters sometime before a return statement. In fact, the out-parameters act very much like local variables, and can be assigned to more than once. Return statements are used when one wants to return before reaching the end of the body block of the method.
Return statements can be just the return
keyword (where the current values
of the out-parameters are used), or they can take a list of expressions to
return. If a list is given, the number of expressions given must be the same
as the number of named out-parameters. These expressions are
evaluated, then they are assigned to the out-parameters, and then the
method terminates.
8.4. Yield Statement (grammar) {#sec-yield-statement}
A yield statement may only be used in an iterator. See iterator types for more details about iterators.
The body of an iterator is a co-routine. It is used
to yield control to its caller, signaling that a new
set of values for the iterator's yield (out-)parameters (if any)
are available. Values are assigned to the yield parameters
at or before a yield statement.
In fact, the yield parameters act very much like local variables,
and can be assigned to more than once. Yield statements are
used when one wants to return new yield parameter values
to the caller. Yield statements can be just the
yield
keyword (where the current values of the yield parameters
are used), or they can take a list of expressions to yield.
If a list is given, the number of expressions given must be the
same as the number of named iterator out-parameters.
These expressions are then evaluated, then they are
assigned to the yield parameters, and then the iterator
yields.
8.5. Update and Call Statements (grammar) {#sec-update-and-call-statement}
Examples:
class C { var f: int }
class D {
var i: int
constructor(i: int) {
this.i := i;
}
}
method q(i: int, j: int) {}
method r() returns (s: int, t: int) { return 2,3; }
method m() {
var ss: int, tt: int, c: C?, a: array<int>, d: D?;
q(0,1);
ss, c.f := r();
c := new C;
d := new D(2);
a := new int[10];
ss, tt := 212, 33;
ss :| ss > 7;
ss := *;
}
This statement corresponds to familiar assignment or method call statements, with variations. If more than one left-hand side is used, these must denote different l-values, unless the corresponding right-hand sides also denote the same value.
The update statement serves several logical purposes.
- Examples of method calls take this form
m();
m(1,2,3) {:attr} ;
e.f().g.m(45);
As there are no left-hand-side locations to receive values, this form is allowed only for methods that have no out-parameters.
This form uses :=
to denote the assignment of the out-parameters of the method to the
corresponding number of LHS values.
a, b.e().f := m() {:attr};
In this case, the right-hand-side must be a method call and the number of left-hand sides must match the number of out-parameters of the method that is called. Note that the result of a method call is not allowed to be used as an argument of another method call, as if it were an expression.
A parallel-assignment has one-or-more right-hand-side expressions, which may be function calls but may not be method calls.
x, y := y, x;
The above example swaps the values of x
and y
. If more than one
left-hand side is used, these must denote different l-values, unless the
corresponding right-hand sides also denote the same value. There must
be an equal number of left-hand sides and right-hand sides.
The most common case has only one RHS and one LHS.
The form with a right-hand-side that is *
is a havoc assignment.
It assigns an arbitrary but type-correct value to the corresponding left-hand-side.
It can be mixed with other assignments of computed values.
a := *;
a, b, c := 4, *, 5;
This form has one or more left-hand-sides, a :|
symbol and then a boolean expression on the right.
The effect is to assign values to the left-hand-sides that satisfy the
RHS condition.
x, y :| 0 < x+y < 10;
This is read as assign values to x
and y
such that 0 < x+y < 10
is true.
The given boolean expression need not constrain the LHS values uniquely:
the choice of satisfying values is non-deterministic.
This can be used to make a choice as in the
following example where we choose an element in a set.
method Sum(X: set<int>) returns (s: int)
{
s := 0; var Y := X;
while Y != {}
decreases Y
{
var y: int;
y :| y in Y;
s, Y := s + y, Y - {y};
}
}
Dafny will report an error if it cannot prove that values exist that satisfy the condition.
In this variation, with an assume
keyword
y :| assume y in Y;
Dafny assumes without proof that an appropriate value exists.
Note that the syntax
Lhs ":"
is interpreted as a label in which the user forgot the label
keyword.
The purpose of this form of a method call is to seperate the called method's precondition and its proof from the rest of the correctness proof of the calling method.
opaque predicate P() { true }
lemma ProveP() ensures P() {
reveal P();
}
method M(i: int) returns (r: int)
requires P()
ensures r == i
{ r := i; }
method C() {
var v := M(1/3) by { // We prove 3 != 0 outside of the by proof
ProveP(); // Prove precondtion
}
assert v == 0; // Use postcondition
assert P(); // Fails
}
By placing the call to lemma ProveP
inside of the by block, we can not use
P
after the method call. The well-formedness checks of the arguments to the
method call are not subject to the separation.
8.6. Update with Failure Statement (:-
) (grammar) {#sec-update-with-failure-statement}
See the subsections below for examples.
A :-
1 statement is an alternate form of the :=
statement that allows for abrupt return if a failure is detected.
This is a language feature somewhat analogous to exceptions in other languages.
An update-with-failure statement uses failure-compatible types. A failure-compatible type is a type that has the following (non-static) members (each with no in-parameters and one out-parameter):
- a non-ghost function
IsFailure()
that returns abool
- an optional non-ghost function
PropagateFailure()
that returns a value assignable to the first out-parameter of the caller - an optional function
Extract()
(PropagateFailure and Extract were permitted to be methods (but deprecated) prior to Dafny 4. They will be required to be functions in Dafny 4.)
A failure-compatible type with an Extract
member is called value-carrying.
To use this form of update,
- if the RHS of the update-with-failure statement is a method call, the first out-parameter of the callee must be failure-compatible
- if instead, the RHS of the update-with-failure statement is one or more expressions, the first of these expressions must be a value with a failure-compatible type
- the caller must have a first out-parameter whose type matches the output of
PropagateFailure
applied to the first output of the callee, unless anexpect
,assume
, orassert
keyword is used after:-
(cf. Section 8.6.7). - if the failure-compatible type of the RHS does not have an
Extract
member, then the LHS of the:-
statement has one less expression than the RHS (or than the number of out-parameters from the method call), the value of the first out-parameter or expression being dropped (see the discussion and examples in Section 8.6.2) - if the failure-compatible type of the RHS does have an
Extract
member, then the LHS of the:-
statement has the same number of expressions as the RHS (or as the number of out-parameters from the method call) and the type of the first LHS expression must be assignable from the return type of theExtract
member - the
IsFailure
andPropagateFailure
methods may not be ghost - the LHS expression assigned the output of the
Extract
member is ghost precisely ifExtract
is ghost
The following subsections show various uses and alternatives.
A simple failure-compatible type is the following:
datatype Status =
| Success
| Failure(error: string)
{
predicate IsFailure() { this.Failure? }
function PropagateFailure(): Status
requires IsFailure()
{
Failure(this.error)
}
}
A commonly used alternative that carries some value information is something like this generic type:
datatype Outcome<T> =
| Success(value: T)
| Failure(error: string)
{
predicate IsFailure() {
this.Failure?
}
function PropagateFailure<U>(): Outcome<U>
requires IsFailure()
{
Failure(this.error) // this is Outcome<U>.Failure(...)
}
function Extract(): T
requires !IsFailure()
{
this.value
}
}
The simplest use of this failure-return style of programming is to have a method call that just returns a non-value-carrying Status
value:
method Callee(i: int) returns (r: Status)
{
if i < 0 { return Failure("negative"); }
return Success;
}
method Caller(i: int) returns (rr: Status)
{
:- Callee(i);
...
}
Note that there is no LHS to the :-
statement.
If Callee
returns Failure
, then the caller immediately returns,
not executing any statements following the call of Callee
.
The value returned by Caller
(the value of rr
in the code above) is the result of PropagateFailure
applied to the value returned by Callee
, which is often just the same value.
If Callee
does not return Failure
(that is, returns a value for which IsFailure()
is false
)
then that return value is forgotten and execution proceeds normally with the statements following the call of Callee
in the body of Caller
.
The desugaring of the :- Callee(i);
statement is
var tmp;
tmp := Callee(i);
if tmp.IsFailure() {
rr := tmp.PropagateFailure();
return;
}
In this and subsequent examples of desugaring, the tmp
variable is a new, unique variable, unused elsewhere in the calling member.
The example in the previous subsection affects the program only through side effects or the status return itself.
It may well be convenient to have additional out-parameters, as is allowed for :=
updates;
these out-parameters behave just as for :=
.
Here is an example:
method Callee(i: int) returns (r: Status, v: int, w: int)
{
if i < 0 { return Failure("negative"), 0, 0; }
return Success, i+i, i*i;
}
method Caller(i: int) returns (rr: Status, k: int)
{
var j: int;
j, k :- Callee(i);
k := k + k;
...
}
Here Callee
has two outputs in addition to the Status
output.
The LHS of the :-
statement accordingly has two l-values to receive those outputs.
The recipients of those outputs may be any sort of l-values;
here they are a local variable and an out-parameter of the caller.
Those outputs are assigned in the :-
call regardless of the Status
value:
- If
Callee
returns a failure value as its first output, then the other outputs are assigned, the caller's first out-parameter (hererr
) is assigned the value ofPropagateFailure
, and the caller returns. - If
Callee
returns a non-failure value as its first output, then the other outputs are assigned and the caller continues execution as normal.
The desugaring of the j, k :- Callee(i);
statement is
var tmp;
tmp, j, k := Callee(i);
if tmp.IsFailure() {
rr := tmp.PropagateFailure();
return;
}
The failure-compatible return value can carry additional data as shown in the Outcome<T>
example above.
In this case there is a (first) LHS l-value to receive this additional data. The type of that first LHS
value is one that is assignable from the result of the Extract
function, not the actual first out-parameter.
method Callee(i: int) returns (r: Outcome<nat>, v: int)
{
if i < 0 { return Failure("negative"), i+i; }
return Success(i), i+i;
}
method Caller(i: int) returns (rr: Outcome<int>, k: int)
{
var j: int;
j, k :- Callee(i);
k := k + k;
...
}
Suppose Caller
is called with an argument of 10
.
Then Callee
is called with argument 10
and returns r
and v
of Outcome<nat>.Success(10)
and 20
.
Here r.IsFailure()
is false
, so control proceeds normally.
The j
is assigned the result of r.Extract()
, which will be 10
,
and k
is assigned 20
.
Control flow proceeds to the next line, where k
now gets the value 40
.
Suppose instead that Caller
is called with an argument of -1
.
Then Callee
is called with the value -1
and returns r
and v
with values Outcome<nat>.Failure("negative")
and -2
.
k
is assigned the value of v
(-2).
But r.IsFailure()
is true
, so control proceeds directly to return from Caller
.
The first out-parameter of Caller
(rr
) gets the value of r.PropagateFailure()
,
which is Outcome<int>.Failure("negative")
; k
already has the value -2
.
The rest of the body of Caller
is skipped.
In this example, the first out-parameter of Caller
has a failure-compatible type
so the exceptional return will propagate up the call stack.
It will keep propagating up the call stack
as long as there are callers with this first special output type
and calls that use :-
and the return value keeps having IsFailure()
true.
The desugaring of the j, k :- Callee(i);
statement in this example is
var tmp;
tmp, k := Callee(i);
if tmp.IsFailure() {
rr := tmp.PropagateFailure();
return;
}
j := tmp.Extract();
Instead of a failure-returning method call on the RHS of the statement,
the RHS can instead be a list of expressions.
As for a :=
statement, in this form, the expressions on the left and right sides of :-
must correspond,
just omitting a LHS l-value for the first RHS expression if its type is not value-carrying.
The semantics is very similar to that in the previous subsection.
- The first RHS expression must have a failure-compatible type.
- All the assignments of RHS expressions to LHS values except for the first RHS value are made.
- If the first RHS value (say
r
) respondstrue
tor.IsFailure()
, thenr.PropagateFailure()
is assigned to the first out-parameter of the caller and the execution of the caller's body is ended. - If the first RHS value (say
r
) respondsfalse
tor.IsFailure()
, then- if the type of
r
is value-carrying, thenr.Extract()
is assigned to the first LHS value of the:-
statement; ifr
is not value-carrying, then the corresponding LHS l-value is omitted - execution of the caller's body continues with the statement following the
:-
statement.
- if the type of
A RHS with a method call cannot be mixed with a RHS containing multiple expressions.
For example, the desugaring of
method m(r: Status) returns (rr: Status) {
var k;
k :- r, 7;
...
}
is
var k;
var tmp;
tmp, k := r, 7;
if tmp.IsFailure() {
rr := tmp.PropagateFailure();
return;
}
The :-
syntax can also be used in initialization, as in
var s, t :- M();
This is equivalent to
var s, t;
s, t :- M();
with the semantics as described above.
In any of the above described uses of :-
, the :-
token may be followed immediately by the keyword expect
, assert
or assume
.
assert
means that the RHS evaluation is expected to be successful, but that the verifier should prove that this is so; that is, the verifier should proveassert !r.IsFailure()
(wherer
is the status return from the callee) (cf. Section 8.17)assume
means that the RHS evaluation should be assumed to be successful, as if the statementassume !r.IsFailure()
followed the evaluation of the RHS (cf. Section 8.18)expect
means that the RHS evaluation should be assumed to be successful (like usingassume
above), but that the compiler should include a run-time check for success. This is equivalent to includingexpect !r.IsFailure()
after the RHS evaluation; that is, if the status return is a failure, the program halts. (cf. Section 8.19)
In each of these cases, there is no abrupt return from the caller. Thus
there is no evaluation of PropagateFailure
. Consequently the first
out-parameter of the caller need not match the return type of
PropagateFailure
; indeed, the failure-compatible type returned by the
callee need not have a PropagateFailure
member.
The equivalent desugaring replaces
if tmp.IsFailure() {
rr := tmp.PropagateFailure();
return;
}
with
expect !tmp.IsFailure(), tmp;
or
assert !tmp.IsFailure();
or
assume !tmp.IsFailure();
There is a grammatical nuance that the user should be aware of.
The keywords assert
, assume
, and expect
can start an expression.
For example, assert P; E
can be an expression. However, in
e :- assert P; E;
the assert
is parsed as the keyword associated with
:-
. To have the assert
considered part of the expression use parentheses:
e :- (assert P; E);
.
There are several points to note.
- The first out-parameter of the callee is special. It has a special type and that type indicates that the value is inspected to see if an abrupt return from the caller is warranted. This type is often a datatype, as shown in the examples above, but it may be any type with the appropriate members.
- The restriction on the type of caller's first out-parameter is
just that it must be possible (perhaps through generic instantiation and type inference, as in these examples)
for
PropagateFailure
applied to the failure-compatible output from the callee to produce a value of the caller's first out-parameter type. If the caller's first out-parameter type is failure-compatible (which it need not be), then failures can be propagated up the call chain. If the keyword form (e.g.assume
) of the statement is used, then noPropagateFailure
member is needed, because no failure can occur, and there is no restriction on the caller's first out-parameter. - In the statement
j, k :- Callee(i);
, when the callee's return value has anExtract
member, the type ofj
is not the type of the first out-parameter ofCallee
. Rather it is a type assignable from the output type ofExtract
applied to the first out-value ofCallee
. - A method like
Callee
with a special first out-parameter type can still be used in the normal way:r, k := Callee(i)
. Nowr
gets the first output value fromCallee
, of typeStatus
orOutcome<nat>
in the examples above. No special semantics or exceptional control paths apply. Subsequent code can do its own testing of the value ofr
and whatever other computations or control flow are desired. - The caller and callee can have any (positive) number of output arguments,
as long as the callee's first out-parameter has a failure-compatible type
and the caller's first out-parameter type matches
PropagateFailure
. - If there is more than one LHS, the LHSs must denote different l-values, unless the RHS is a list of expressions and the corresponding RHS values are equal.
- The LHS l-values are evaluated before the RHS method call, in case the method call has side-effects or return values that modify the l-values prior to assignments being made.
It is important to note the connection between the failure-compatible types used in the caller and callee,
if they both use them.
They do not have to be the same type, but they must be closely related,
as it must be possible for the callee's PropagateFailure
to return a value of the caller's failure-compatible type.
In practice this means that one such failure-compatible type should be used for an entire program.
If a Dafny program uses a library shared by multiple programs, the library should supply such a type
and it should be used by all the client programs (and, effectively, all Dafny libraries).
It is also the case that it is inconvenient to mix types such as Outcome
and Status
above within the same program.
If there is a mix of failure-compatible types, then the program will need to use :=
statements and code for
explicit handling of failure values.
The :-
mechanism is like the exceptions used in other programming languages, with some similarities and differences.
- There is essentially just one kind of 'exception' in Dafny, the variations of the failure-compatible data type.
- Exceptions are passed up the call stack whether or not intervening methods are aware of the possibility of an exception,
that is, whether or not the intervening methods have declared that they throw exceptions.
Not so in Dafny: a failure is passed up the call stack only if each caller has a failure-compatible first out-parameter, is itself called in a
:-
statement, and returns a value that responds true toIsFailure()
. - All methods that contain failure-return callees must explicitly handle those failures
using either
:-
statements or using:=
statements with a LHS to receive the failure value.
8.7. Variable Declaration Statement (grammar) {#sec-variable-declaration-statement}
Examples:
method m() {
var x, y: int; // x's type is inferred, not necessarily 'int'
var b: bool, k: int;
x := 1; // settles x's type
}
A variable declaration statement is used to declare one or more local variables in a method or function. The type of each local variable must be given unless its type can be inferred, either from a given initial value, or from other uses of the variable. If initial values are given, the number of values must match the number of variables declared.
The scope of the declared variable extends to the end of the block in which it is
declared. However, be aware that if a simple variable declaration is followed
by an expression (rather than a subsequent statement) then the var
begins a
Let Expression and the scope of the introduced variables is
only to the end of the expression. In this case, though, the var
is in an expression
context, not a statement context.
Note that the type of each variable must be given individually. The following code
var x, y : int;
var x, y := 5, 6;
var x, y :- m();
var x, y :| 0 < x + y < 10;
var (x, y) := makePair();
var Cons(x, y) = ConsMaker();
does not declare both x
and y
to be of type int
. Rather it will give an
error explaining that the type of x
is underspecified if it cannot be
inferred from uses of x.
The variables can be initialized with syntax similar to update statements (cf. Section 8.5).
If the RHS is a call, then any variable receiving the value of a
formal ghost out-parameter will automatically be declared as ghost, even
if the ghost
keyword is not part of the variable declaration statement.
The left-hand side can also contain a tuple of patterns that will be matched against the right-hand-side. For example:
function returnsTuple() : (int, int)
{
(5, 10)
}
function usesTuple() : int
{
var (x, y) := returnsTuple();
x + y
}
The initialization with failure operator :-
returns from the enclosing method if the initializer evaluates to a failure value of a failure-compatible type (see Section 8.6).
8.8. Guards (grammar) {#sec-guard}
Examples (in if
statements):
method m(i: int) {
if (*) { print i; }
if i > 0 { print i; }
}
Guards are used in if
and while
statements as boolean expressions. Guards
take two forms.
The first and most common form is just a boolean expression.
The second form is either *
or (*)
. These have the same meaning. An
unspecified boolean value is returned. The value returned
may be different each time it is executed.
8.9. Binding Guards (grammar) {#sec-binding-guards}
Examples (in if
statements):
method m(i: int) {
ghost var k: int;
if i, j :| 0 < i+j < 10 {
k := 0;
} else {
k := 1;
}
}
An if
statement can also take a binding guard.
Such a guard checks if there exist values for the given variables that satisfy the given expression.
If so, it binds some satisfying values to the variables and proceeds
into the "then" branch; otherwise it proceeds with the "else" branch,
where the bound variables are not in scope.
In other words, the statement
if x :| P { S } else { T }
has the same meaning as
if exists x :: P { var x :| P; S } else { T }
The identifiers bound by the binding guard are ghost variables and cannot be assigned to non-ghost variables. They are only used in specification contexts.
Here is another example:
predicate P(n: int)
{
n % 2 == 0
}
method M1() returns (ghost y: int)
requires exists x :: P(x)
ensures P(y)
{
if x : int :| P(x) {
y := x;
}
}
8.10. If Statement (grammar) {#sec-if-statement}
Examples:
method m(i: int) {
var x: int;
if i > 0 {
x := i;
} else {
x := -i;
}
if * {
x := i;
} else {
x := -i;
}
if i: nat, j: nat :| i+j<10 {
assert i < 10;
}
if i == 0 {
x := 0;
} else if i > 0 {
x := 1;
} else {
x := -1;
}
if
case i == 0 => x := 0;
case i > 0 => x := 1;
case i < 0 => x := -1;
}
The simplest form of an if
statement uses a guard that is a boolean
expression. For example,
if x < 0 {
x := -x;
}
Unlike match
statements, if
statements do not have to be exhaustive:
omitting the else
block is the same as including an empty else
block. To ensure that an if
statement is exhaustive, use the
if-case
statement documented below.
If the guard is an asterisk then a non-deterministic choice is made:
if * {
print "True";
} else {
print "False";
}
The then alternative of the if-statement must be a block statement; the else alternative may be either a block statement or another if statement. The condition of the if statement need not (but may) be enclosed in parentheses.
An if statement with a binding guard is non-deterministic;
it will not be compiled if --enforce-determinism
is enabled
(even if it can be proved that there is a unique value).
An if statement with *
for a guard is non-deterministic and ghost.
The if-case
statement using the AlternativeBlock
form is similar to the
if ... fi
construct used in the book "A Discipline of Programming" by
Edsger W. Dijkstra. It is used for a multi-branch if
.
For example:
method m(x: int, y: int) returns (max: int)
{
if {
case x <= y => max := y;
case y <= x => max := x;
}
}
In this form, the expressions following the case
keyword are called
guards. The statement is evaluated by evaluating the guards in an
undetermined order until one is found that is true
and the statements
to the right of =>
for that guard are executed. The statement requires
at least one of the guards to evaluate to true
(that is, if-case
statements must be exhaustive: the guards must cover all cases).
In the if-with-cases, a sequence of statements may follow the =>
; it
may but need not be a block statement (a brace-enclosed sequence of statements).
The form that used ...
(a refinement feature) as the guard is deprecated.
8.11. Match Statement (grammar) {#sec-match-statement}
Examples:
match list {
case Nil => {}
case Cons(head,tail) => print head;
}
match x
case 1 =>
print x;
case 2 =>
var y := x*x;
print y;
case _ =>
print "Other";
// Any statement after is captured in this case.
The match
statement is used to do case analysis on a value of an expression.
The expression may be a value of a basic type (e.g. int
), a newtype, or
an inductive or coinductive datatype (which includes the built-in tuple types).
The expression after the match
keyword is called the selector.
The selector is evaluated and then matched against
each clause in order until a matching clause is found.
The process of matching the selector expression against the case patterns is the same as for match expressions and is described in Section 9.31.2.
The selector need not be enclosed in parentheses; the sequence of cases may but need not be enclosed in braces.
The cases need not be disjoint.
The cases must be exhaustive, but you can use a wild variable (_
) or a simple identifier to indicate "match anything".
Please refer to the section about case patterns to learn more about shadowing, constants, etc.
The code below shows an example of a match statement.
datatype Tree = Empty | Node(left: Tree, data: int, right: Tree)
// Return the sum of the data in a tree.
method Sum(x: Tree) returns (r: int)
{
match x {
case Empty => r := 0;
case Node(t1, d, t2) =>
var v1 := Sum(t1);
var v2 := Sum(t2);
r := v1 + d + v2;
}
}
Note that the Sum
method is recursive yet has no decreases
annotation.
In this case it is not needed because Dafny is able to deduce that
t1
and t2
are smaller (structurally) than x
. If Tree
had been
coinductive this would not have been possible since x
might have been
infinite.
8.12. While Statement (grammar) {#sec-while-statement}
Examples:
method m() {
var i := 10;
while 0 < i
invariant 0 <= i <= 10
decreases i
{
i := i-1;
}
while * {}
i := *;
while
decreases if i < 0 then -i else i
{
case i < 0 => i := i + 1;
case i > 0 => i := i - 1;
}
}
Loops
- may be a conventional loop with a condition and a block statement for a body
- need not have parentheses around the condition
- may have a
*
for the condition (the loop is then non-deterministic) - binding guards are not allowed
- may have a case-based structure
- may have no body --- a bodyless loop is not compilable, but can be reaosnaed about
Importantly, loops need loop specifications in order for Dafny to prove that they obey expected behavior. In some cases Dafny can infer the loop specifications by analyzing the code, so the loop specifications need not always be explicit. These specifications are described in Section 7.6 and Section 8.15.
The general loop statement in Dafny is the familiar while
statement.
It has two general forms.
The first form is similar to a while loop in a C-like language. For example:
method m(){
var i := 0;
while i < 5 {
i := i + 1;
}
}
In this form, the condition following the while
is one of these:
- A boolean expression. If true it means execute one more iteration of the loop. If false then terminate the loop.
- An asterisk (
*
), meaning non-deterministically yield eithertrue
orfalse
as the value of the condition
The body of the loop is usually a block statement, but it can also be missing altogether. A loop with a missing body may still pass verification, but any attempt to compile the containing program will result in an error message. When verifying a loop with a missing body, the verifier will skip attempts to prove loop invariants and decreases assertions that would normally be asserted at the end of the loop body. There is more discussion about bodyless loops in Section 8.15.4.
The second form uses a case-based block. It is similar to the
do ... od
construct used in the book "A Discipline of Programming" by
Edsger W. Dijkstra. For example:
method m(n: int){
var r := n;
while
decreases if 0 <= r then r else -r
{
case r < 0 =>
r := r + 1;
case 0 < r =>
r := r - 1;
}
}
For this form, the guards are evaluated in some undetermined order until one is found that is true, in which case the corresponding statements are executed and the while statement is repeated. If none of the guards evaluates to true, then the loop execution is terminated.
The form that used ...
(a refinement feature) as the guard is deprecated.
8.13. For Loops (grammar) {#sec-for-statement}
Examples:
method m() decreases * {
for i := 0 to 10 {}
for _ := 0 to 10 {}
for i := 0 to * invariant i >= 0 decreases * {}
for i: int := 10 downto 0 {}
for i: int := 10 downto 0
}
The for
statement provides a convenient way to write some common loops.
The statement introduces a local variable with optional type, which is called
the loop index. The loop index is in scope in the specification and the body,
but not after the for
loop. Assignments to the loop index are not allowed.
The type of the loop index can typically be inferred; if so, it need not be given
explicitly. If the identifier is not used, it can be written as _
, as illustrated
in this repeat-20-times loop:
for _ := 0 to 20 {
Body
}
There are four basic variations of the for
loop:
for i: T := lo to hi
LoopSpec
{ Body }
for i: T := hi downto lo
LoopSpec
{ Body }
for i: T := lo to *
LoopSpec
{ Body }
for i: T := hi downto *
LoopSpec
{ Body }
Semantically, they are defined as the following respective while
loops:
{
var _lo, _hi := lo, hi;
assert _lo <= _hi && forall _i: int :: _lo <= _i <= _hi ==> _i is T;
var i := _lo;
while i != _hi
invariant _lo <= i <= _hi
LoopSpec
decreases _hi - i
{
Body
i := i + 1;
}
}
{
var _lo, _hi := lo, hi;
assert _lo <= _hi && forall _i: int :: _lo <= _i <= _hi ==> _i is T;
var i := _hi;
while i != lo
invariant _lo <= i <= _hi
LoopSpec
decreases i - _lo
{
i := i - 1;
Body
}
}
{
var _lo := lo;
assert forall _i: int :: _lo <= _i ==> _i is T;
var i := _lo;
while true
invariant _lo <= i
LoopSpec
{
Body
i := i + 1;
}
}
{
var _hi := hi;
assert forall _i: int :: _i <= _hi ==> _i is T;
var i := _hi;
while true
invariant i <= _hi
LoopSpec
{
i := i - 1;
Body
}
}
The expressions lo
and hi
are evaluated just once, before the loop
iterations start.
Also, in all variations the values of i
in the body are the values
from lo
to, but not including, hi
. This makes it convenient to
write common loops, including these:
for i := 0 to a.Length {
Process(a[i]);
}
for i := a.Length downto 0 {
Process(a[i]);
}
Nevertheless, hi
must be a legal value for the type of the index variable,
since that is how the index variable is used in the invariant.
If the end-expression is not *
, then no explicit decreases
is
allowed, since such a loop is already known to terminate.
If the end-expression is *
, then the absence of an explicit decreases
clause makes it default to decreases *
. So, if the end-expression is *
and no
explicit decreases
clause is given, the loop is allowed only in methods
that are declared with decreases *
.
The directions to
or downto
are contextual keywords. That is, these two
words are part of the syntax of the for
loop, but they are not reserved
keywords elsewhere.
Just like for while loops, the body of a for-loop may be omitted during verification. This suppresses attempts to check assertions (like invariants) that would occur at the end of the loop. Eventually, however a body must be provided; the compiler will not compile a method containing a body-less for-loop. There is more discussion about bodyless loops in Section 8.15.4.
8.14. Break and Continue Statements (grammar) {#sec-break-continue-statement}
Examples:
class A { var f: int }
method m(a: A) {
label x:
while true {
if (*) { break; }
}
label y: {
var z := 1;
if * { break y; }
z := 2;
}
}
Break and continue statements provide a means to transfer control in a way different than the usual nested control structures. There are two forms of each of these statements: with and without a label.
If a label is used, the break or continue statement must be enclosed in a statement with that label. The enclosing statement is called the target of the break or continue.
A break
statement transfers control to the point immediately
following the target statement. For example, such a break statement can be
used to exit a sequence of statements in a block statement before
reaching the end of the block.
For example,
label L: {
var n := ReadNext();
if n < 0 {
break L;
}
DoSomething(n);
}
is equivalent to
{
var n := ReadNext();
if 0 <= n {
DoSomething(n);
}
}
If no label is specified and the statement lists n
occurrences of break
, then the statement must be enclosed in
at least n
levels of loop statements. Control continues after exiting n
enclosing loops. For example,
method m() {
for i := 0 to 10 {
for j := 0 to 10 {
label X: {
for k := 0 to 10 {
if j + k == 15 {
break break;
}
}
}
}
// control continues here after the "break break", exiting two loops
}
}
Note that a non-labeled break
pays attention only to loops, not to labeled
statements. For example, the labeled block X
in the previous example
does not play a role in determining the target statement of the break break;
.
For a continue
statement, the target statement must be a loop statement.
The continue statement transfers control to the point immediately
before the closing curly-brace of the loop body.
For example,
method m() {
for i := 0 to 100 {
if i == 17 {
continue;
}
DoSomething(i);
}
}
method DoSomething(i:int){}
is equivalent to
method m() {
for i := 0 to 100 {
if i != 17 {
DoSomething(i);
}
}
}
method DoSomething(i:int){}
The same effect can also be obtained by wrapping the loop body in a labeled
block statement and then using break
with a label, but that usually makes
for a more cluttered program:
method m() {
for i := 0 to 100 {
label LoopBody: {
if i == 17 {
break LoopBody;
}
DoSomething(i);
}
}
}
method DoSomething(i:int){}
Stated differently, continue
has the effect of ending the current loop iteration,
after which control continues with any remaining iterations. This is most natural
for for
loops. For a while
loop, be careful to make progress toward termination
before a continue
statement. For example, the following program snippet shows
an easy mistake to make (the verifier will complain that the loop may not terminate):
method m() {
var i := 0;
while i < 100 {
if i == 17 {
continue; // error: this would cause an infinite loop
}
DoSomething(i);
i := i + 1;
}
}
method DoSomething(i:int){}
The continue
statement can give a label, provided the label is a label of a loop.
For example,
method m() {
label Outer:
for i := 0 to 100 {
for j := 0 to 100 {
if i + j == 19 {
continue Outer;
}
WorkIt(i, j);
}
PostProcess(i);
// the "continue Outer" statement above transfers control to here
}
}
method WorkIt(i:int, j:int){}
method PostProcess(i:int){}
If a non-labeled continue statement lists n
occurrences of break
before the
continue
keyword, then the statement must be enclosed in at least n + 1
levels
of loop statements. The effect is to break
out of the n
most closely enclosing
loops and then continue
the iterations of the next loop. That is, n
occurrences
of break
followed by one more break;
will break out of n
levels of loops
and then do a break
, whereas n
occurrences of break
followed by continue;
will break out of n
levels of loops and then do a continue
.
For example, the WorkIt
example above can equivalently be written without labels
as
method m() {
for i := 0 to 100 {
for j := 0 to 100 {
if i + j == 19 {
break continue;
}
WorkIt(i, j);
}
PostProcess(i);
// the "break continue" statement above transfers control to here
}
}
method WorkIt(i:int, j:int){}
method PostProcess(i:int){}
Note that a loop invariant is checked on entry to a loop and at the closing curly-brace
of the loop body. It is not checked at break statements. For continue statements,
the loop invariant is checked as usual at the closing curly-brace
that the continue statement jumps to.
This checking ensures that the loop invariant holds at the very top of
every iteration. Commonly, the only exit out of a loop happens when the loop guard evaluates
to false
. Since no state is changed between the top of an iteration (where the loop
invariant is known to hold) and the evaluation of the loop guard, one can also rely on
the loop invariant to hold immediately following the loop. But the loop invariant may
not hold immediately following a loop if a loop iteration changes the program state and
then exits the loop with a break statement.
For example, the following program verifies:
method m() {
var i := 0;
while i < 10
invariant 0 <= i <= 10
{
if P(i) {
i := i + 200;
break;
}
i := i + 1;
}
assert i == 10 || 200 <= i < 210;
}
predicate P(i:int)
To explain the example, the loop invariant 0 <= i <= 10
is known to hold at the very top
of each iteration,
that is, just before the loop guard i < 10
is evaluated. If the loop guard evaluates
to false
, then the negated guard condition (10 <= i
) and the invariant hold, so
i == 10
will hold immediately after the loop. If the loop guard evaluates to true
(that is, i < 10
holds), then the loop body is entered. If the test P(i)
then evaluates
to true
, the loop adds 200
to i
and breaks out of the loop, so on such a
path, 200 <= i < 210
is known to hold immediately after the loop. This is summarized
in the assert statement in the example.
So, remember, a loop invariant holds at the very top of every iteration, not necessarily
immediately after the loop.
For some simple loops, such as those mentioned previously, Dafny can figure out what the loop is doing without more help. However, in general the user must provide more information in order to help Dafny prove the effect of the loop. This information is provided by a loop specification. A loop specification provides information about invariants, termination, and what the loop modifies. For additional tutorial information see [@KoenigLeino:MOD2011] or the online Dafny tutorial.
Loops present a problem for specification-based reasoning. There is no way to know in advance how many times the code will go around the loop and a tool cannot reason about every one of a possibly unbounded sequence of unrollings. In order to consider all paths through a program, specification-based program verification tools require loop invariants, which are another kind of annotation.
A loop invariant is an expression that holds just prior to the loop test, that is, upon entering a loop and after every execution of the loop body. It captures something that is invariant, i.e. does not change, about every step of the loop. Now, obviously we are going to want to change variables, etc. each time around the loop, or we wouldn't need the loop. Like pre- and postconditions, an invariant is a property that is preserved for each execution of the loop, expressed using the same boolean expressions we have seen. For example,
var i := 0;
while i < n
invariant 0 <= i
{
i := i + 1;
}
When you specify an invariant, Dafny proves two things: the invariant holds upon entering the loop, and it is preserved by the loop. By preserved, we mean that assuming that the invariant holds at the beginning of the loop (just prior to the loop test), we must show that executing the loop body once makes the invariant hold again. Dafny can only know upon analyzing the loop body what the invariants say, in addition to the loop guard (the loop condition). Just as Dafny will not discover properties of a method on its own, it will not know that any but the most basic properties of a loop are preserved unless it is told via an invariant.
Dafny proves that code terminates, i.e. does not loop forever, by using
decreases
annotations. For many things, Dafny is able to guess the right
annotations, but sometimes it needs to be made explicit.
There are two places Dafny proves termination: loops and recursion.
Both of these situations require either an explicit annotation or a
correct guess by Dafny.
A decreases
annotation, as its name suggests, gives Dafny an expression
that decreases with every loop iteration or recursive call. There are two
conditions that Dafny needs to verify when using a decreases
expression:
- that the expression actually gets smaller, and
- that it is bounded.
That is, the expression must strictly decrease in a well-founded ordering (cf. Section 12.7).
Many times, an integral value (natural or plain integer) is the quantity
that decreases, but other values can be used as well. In the case of
integers, the bound is assumed to be zero.
For each loop iteration the decreases
expression at the end of the loop
body must be strictly smaller than its value at the beginning of the loop
body (after the loop test). For integers, the well-founded relation between
x
and X
is x < X && 0 <= X
.
Thus if the decreases
value (X
) is negative at the
loop test, it must exit the loop, since there is no permitted value for
x
to have at the end of the loop body.
For example, the following is
a proper use of decreases
on a loop:
method m(n: nat){
var i := n;
while 0 < i
invariant 0 <= i
decreases i
{
i := i - 1;
}
}
Here Dafny has all the ingredients it needs to prove termination. The
variable i
becomes smaller each loop iteration, and is bounded below by
zero. When i
becomes 0, the lower bound of the well-founded order, control
flow exits the loop.
This is fine, except the loop is backwards compared to most loops, which tend to count up instead of down. In this case, what decreases is not the counter itself, but rather the distance between the counter and the upper bound. A simple trick for dealing with this situation is given below:
method m(m: nat, n: int)
requires m <= n
{
var i := m;
while i < n
invariant 0 <= i <= n
decreases n - i
{
i := i + 1;
}
}
This is actually Dafny's guess for this situation, as it sees i < n
and
assumes that n - i
is the quantity that decreases. The upper bound of the
loop invariant implies that 0 <= n – i
, and gives Dafny a lower bound on
the quantity. This also works when the bound n
is not constant, such as
in the binary search algorithm, where two quantities approach each other,
and neither is fixed.
If the decreases
clause of a loop specifies *
, then no
termination check will be performed. Use of this feature is sound only with
respect to partial correctness.
The specification of a loop also includes framing, which says what the loop modifies. The loop frame includes both local variables and locations in the heap.
For local variables, the Dafny verifier performs a syntactic scan of the loop body to find every local variable or out-parameter that occurs as a left-hand side of an assignment. These variables are called syntactic assignment targets of the loop, or syntactic loop targets for short. Any local variable or out-parameter that is not a syntactic assignment target is known by the verifier to remain unchanged by the loop.
The heap may or may not be a syntactic loop target. It is when the loop body
syntactically contains a statement that can modify a heap location. This
includes calls to compiled methods, even if such a method has an empty
modifies
clause, since a compiled method is always allowed to allocate
new objects and change their values in the heap.
If the heap is not a syntactic loop target, then the verifier knows the heap
remains unchanged by the loop. If the heap is a syntactic loop target,
then the loop's effective modifies
clause determines what is allowed to be
modified by iterations of the loop body.
A loop can use modifies
clauses to declare the effective modifies
clause
of the loop. If a loop does not explicitly declare any modifies
clause, then
the effective modifies
clause of the loop is the effective modifies
clause
of the most tightly enclosing loop or, if there is no enclosing loop, the
modifies
clause of the enclosing method.
In most cases, there is no need to give an explicit modifies
clause for a
loop. The one case where it is sometimes needed is if a loop modifies less
than is allowed by the enclosing method. Here are two simple methods that
illustrate this case:
class Cell {
var data: int
}
method M0(c: Cell, d: Cell)
requires c != d
modifies c, d
ensures c.data == d.data == 100
{
c.data, d.data := 100, 0;
var i := 0;
while i < 100
invariant d.data == i
// Needs "invariant c.data == 100" or "modifies d" to verify
{
d.data := d.data + 1;
i := i + 1;
}
}
method M1(c: Cell)
modifies c
ensures c.data == 100
{
c.data := 100;
var i := 0;
while i < 100
// Needs "invariant c.data == 100" or "modifies {}" to verify
{
var tmp := new Cell;
tmp.data := i;
i := i + 1;
}
}
In M0
, the effective modifies
clause of the loop is modifies c, d
. Therefore,
the method's postcondition c.data == 100
is not provable. To remedy the situation,
the loop needs to be declared either with invariant c.data == 100
or with
modifies d
.
Similarly, the effective modifies
clause of the loop in M1
is modifies c
. Therefore,
the method's postcondition c.data == 100
is not provable. To remedy the situation,
the loop needs to be declared either with invariant c.data == 100
or with
modifies {}
.
When a loop has an explicit modifies
clause, there is, at the top of
every iteration, a proof obligation that
- the expressions given in the
modifies
clause are well-formed, and - everything indicated in the loop
modifies
clause is allowed to be modified by the (effectivemodifies
clause of the) enclosing loop or method.
Methods (including lemmas), functions, loops, and forall
statements are ordinarily
declared with a body, that is, a curly-braces pair that contains (for methods, loops, and forall
)
a list of zero-or-more statements or (for a function) an expression. In each case, Dafny syntactically
allows these constructs to be given without a body (no braces at all). This is to allow programmers to
temporarily postpone the development of the implementation of the method, function, loop, or
aggregate statement.
If a method has no body, there is no difference for callers of the method. Callers still reason
about the call in terms of the method's specification. But without a body, the verifier has
no method implementation to check against the specification, so the verifier is silently happy.
The compiler, on the other hand, will complain if it encounters a body-less method, because the
compiler is supposed to generate code for the method, but it isn't clever enough to do that by
itself without a given method body. If the method implementation is provided by code written
outside of Dafny, the method can be marked with an {:extern}
annotation, in which case the
compiler will no longer complain about the absence of a method body; the verifier will not
object either, even though there is now no proof that the Dafny specifications are satisfied
by the external implementation.
A lemma is a special kind of (ghost) method. Callers are therefore unaffected by the absence of a body,
and the verifier is silently happy with not having a proof to check against the lemma specification.
Despite a lemma being ghost, it is still the compiler that checks for, and complains about,
body-less lemmas. A body-less lemma is an unproven lemma, which is often known as an axiom.
If you intend to use a lemma as an axiom, omit its body and add the attribute {:axiom}
, which
causes the compiler to suppress its complaint about the lack of a body.
Similarly, calls to a body-less function use only the specification of the function. The
verifier is silently happy, but the compiler complains (whether or not the function is ghost).
As for methods and lemmas, the {:extern}
and {:axiom}
attributes can be used to suppress the
compiler's complaint.
By supplying a body for a method or function, the verifier will in effect show the feasibility of
the specification of the method or function. By supplying an {:extern}
or {:axiom}
attribute,
you are taking that responsibility into your own hands. Common mistakes include forgetting to
provide an appropriate modifies
or reads
clause in the specification, or forgetting that
the results of functions in Dafny (unlike in most other languages) must be deterministic.
Just like methods and functions have two sides, callers and implementations, loops also have two sides. One side (analogous to callers) is the context that uses the loop. That context treats the loop in the same way, using its specifications, regardless of whether or not the loop has a body. The other side is the loop body, that is, the implementation of each loop iteration. The verifier checks that the loop body maintains the loop invariant and that the iterations will eventually terminate, but if there is no loop body, the verifier is silently happy. This allows you to temporarily postpone the authoring of the loop body until after you've made sure that the loop specification is what you need in the context of the loop.
There is one thing that works differently for body-less loops than for loops with bodies.
It is the computation of syntactic loop targets, which become part of the loop frame
(see Section 8.15.3). For a body-less loop, the local variables
computed as part of the loop frame are the mutable variables that occur free in the
loop specification. The heap is considered a part of the loop frame if it is used
for mutable fields in the loop specification or if the loop has an explicit modifies
clause.
The IDE will display the computed loop frame in hover text.
For example, consider
class Cell {
var data: int
const K: int
}
method BodylessLoop(n: nat, c: Cell)
requires c.K == 8
modifies c
{
c.data := 5;
var a, b := n, n;
for i := 0 to n
invariant c.K < 10
invariant a <= n
invariant c.data < 10
assert a == n;
assert b == n;
assert c.data == 5;
}
The loop specification mentions local variable a
, and thus a
is considered part of
the loop frame. Since what the loop invariant says about a
is not strong enough to
prove the assertion a == n
that follows the loop, the verifier complains about that
assertion.
Local variable b
is not mentioned in the loop specification, and thus b
is not
included in the loop frame. Since in-parameter n
is immutable, it is not included
in the loop frame, either, despite being mentioned in the loop specification. For
these reasons, the assertion b == n
is provable after the loop.
Because the loop specification mentions the mutable field data
, the heap becomes
part of the loop frame. Since the loop invariant is not strong enough to prove the
assertion c.data == 5
that follows the loop, the verifier complains about that
assertion. On the other hand, had c.data < 10
not been mentioned in the loop
specification, the assertion would be verified, since field K
is then the only
field mentioned in the loop specification and K
is immutable.
Finally, the aggregate statement (forall
) can also be given without a body. Such
a statement claims that the given ensures
clause holds true for all values of
the bound variables that satisfy the given range constraint. If the statement has
no body, the program is in effect omitting the proof, much like a body-less lemma
is omitting the proof of the claim made by the lemma specification. As with the
other body-less constructs above, the verifier is silently happy with a body-less
forall
statement, but the compiler will complain.
8.16. Print Statement (grammar) {#sec-print-statement}
Examples:
print 0, x, list, array;
The print
statement is used to print the values of a comma-separated
list of expressions to the console (standard-out). The generated code uses
target-language-specific idioms to perform this printing.
The expressions may of course include strings that are used
for captions. There is no implicit new line added, so to add a new
line you must include "\n"
as part of one of the expressions.
Dafny automatically creates implementations of methods that convert values to strings
for all Dafny data types. For example,
datatype Tree = Empty | Node(left: Tree, data: int, right: Tree)
method Main()
{
var x : Tree := Node(Node(Empty, 1, Empty), 2, Empty);
print "x=", x, "\n";
}
produces this output:
x=Tree.Node(Tree.Node(Tree.Empty, 1, Tree.Empty), 2, Tree.Empty)
Note that Dafny does not have method overriding and there is no mechanism to
override the built-in value->string conversion. Nor is there a way to
explicitly invoke this conversion.
One can always write an explicit function to convert a data value to a string
and then call it explicitly in a print
statement or elsewhere.
By default, Dafny does not keep track of print effects, but this can be changed
using the --track-print-effects
command line flag. print
statements are allowed
only in non-ghost contexts and not in expressions, with one exception.
The exception is that a function-by-method may contain print
statements,
whose effect may be observed as part of the run-time evaluation of such functions
(unless --track-print-effects
is enabled).
The verifier checks that each expression is well-defined, but otherwise
ignores the print
statement.
Note: print
writes to standard output. To improve compatibility with
native code and external libraries, the process of encoding Dafny strings passed
to print
into standard-output byte strings is left to the runtime of the
language that the Dafny code is compiled to (some language runtimes use UTF-8 in
all cases; others obey the current locale or console encoding).
In most cases, the standard-output encoding can be set before running the
compiled program using language-specific flags or environment variables
(e.g. -Dfile.encoding=
for Java). This is in fact how dafny run
operates:
it uses language-specific flags and variables to enforce UTF-8 output regardless
of the target language (but note that the C++ and Go backends currently have
limited support for UTF-16 surrogates).
8.17. Assert statement (grammar) {#sec-assert-statement}
Examples:
assert i > 0;
assert IsPositive: i > 0;
assert i > 0 by {
...
}
Assert
statements are used to express logical propositions that are
expected to be true. Dafny will attempt to prove that the assertion
is true and give an error if the assertion cannot be proven.
Once the assertion is proved,
its truth may aid in proving subsequent deductions.
Thus if Dafny is having a difficult time verifying a method,
the user may help by inserting assertions that Dafny can prove,
and whose truth may aid in the larger verification effort,
much as lemmas might be used in mathematical proofs.
Assert
statements are ignored by the compiler.
In the by
form of the assert
statement, there is an additional block of statements that provide the Dafny verifier with additional proof steps.
Those statements are often a sequence of lemmas, calc
statements, reveal
statements or other assert
statements,
combined with ghost control flow, ghost variable declarations and ghost update statements of variables declared in the by
block.
The intent is that those statements be evaluated in support of proving the assert
statement.
For that purpose, they could be simply inserted before the assert
statement.
But by using the by
block, the statements in the block are discarded after the assertion is proved.
As a result, the statements in the block do not clutter or confuse the solver in performing subsequent
proofs of assertions later in the program. Furthermore, by isolating the statements in the by
block,
their purpose -- to assist in proving the given assertion -- is manifest in the structure of the code.
Examples of this form of assert are given in the section of the reveal
statement and in Different Styles of Proof
An assert statement may have a label, whose use is explained in Section 8.20.1.
The attributes recognized for assert statements are discussed in Section 11.4.
Using ...
as the argument of the statement is deprecated.
An assert statement can have custom error and success messages.
8.18. Assume Statement (grammar) {#sec-assume-statement}
Examples:
assume i > 0;
assume {:axiom} i > 0 ==> -i < 0;
The assume
statement lets the user specify a logical proposition
that Dafny may assume to be true without proof. If in fact the
proposition is not true this may lead to invalid conclusions.
An assume
statement would ordinarily be used as part of a larger
verification effort where verification of some other part of
the program required the proposition. By using the assume
statement
the other verification can proceed. Then when that is completed the
user would come back and replace the assume
with assert
.
To help the user not forget about that last step, a warning is emitted for any assume statement.
Adding the {:axiom}
attribute to the assume will suppress the warning,
indicating the user takes responsibility for being absolutely sure
that the proposition is indeed true.
Using ...
as the argument of the statement is deprecated.
8.19. Expect Statement (grammar) {#sec-expect-statement}
Examples:
expect i > 0;
expect i > 0, "i is positive";
The expect
statement states a boolean expression that is
(a) assumed to be true by the verifier
and (b) checked to be true
at run-time. That is, the compiler inserts into the run-time executable a
check that the given expression is true; if the expression is false, then
the execution of the program halts immediately. If a second argument is
given, it may be a value of any type.
That value is converted to a string (just like the print
statement)
and the string is included
in the message emitted by the program
when it halts; otherwise a default message is emitted.
Because the expect expression and optional second argument are compiled, they cannot be ghost expressions.
The expect
statement behaves like
assume
for the verifier, but also inserts a run-time check that the
assumption is indeed correct (for the test cases used at run-time).
Here are a few use-cases for the expect
statement.
A) To check the specifications of external methods.
Consider an external method Random
that takes a nat
as input
and returns a nat
value that is less than the input.
Such a method could be specified as
method {:extern} Random(n: nat) returns (r: nat)
ensures r < n
But because there is no body for Random
(only the external non-dafny implementation),
it cannot be verified that Random
actually satisfies this specification.
To mitigate this situation somewhat, we can define a wrapper function, Random'
,
that calls Random
but in which we can put some run-time checks:
method {:extern} Random(n: nat) returns (r: nat)
method Random'(n: nat) returns (r: nat)
ensures r < n
{
r := Random(n);
expect r < n;
}
Here we can verify that Random'
satisfies its own specification,
relying on the unverified specification of Random
.
But we are also checking at run-time that any input-output pairs for Random
encountered during execution
do satisfy the specification,
as they are checked by the expect
statement.
Note, in this example, two problems still remain.
One problem is that the out-parameter of the extern Random
has type nat
,
but there is no check that the value returned really is non-negative.
It would be better to declare the out-parameter of Random
to be int
and
to include 0 <= r
in the condition checked by the expect
statement in Random'
.
The other problem is that Random
surely will need n
to be strictly positive.
This can be fixed by adding requires n != 0
to Random'
and Random
.
B) Run-time testing
Verification and run-time testing are complementary
and both have their role in assuring that software does what is intended.
Dafny can produce executables
and these can be instrumented with unit tests.
Annotating a method with the {:test}
attribute
indicates to the compiler
that it should produce target code
that is correspondingly annotated to mark the method
as a unit test (e.g., an XUnit test) in the target language.
Alternatively, the dafny test
command will produce a main method
that invokes all methods with the {:test}
attribute, and hence does not
depend on any testing framework in the target language.
Within such methods one might use expect
statements (as well as print
statements)
to insert checks that the target program is behaving as expected.
C) Debugging
While developing a new program, one work style uses proof attempts and runtime tests in combination. If an assert statement does not prove, one might run the program with a corresponding expect statement to see if there are some conditions when the assert is not actually true. So one might have paired assert/expect statements:
assert _P_;
expect _P_;
Once the program is debugged, both statements can be removed.
Note that it is important that the assert
come before the expect
, because
by the verifier, the expect
is interpreted as an assume
, which would automatically make
a subsequent assert
succeed.
D) Compiler tests
The same approach might be taken to assure that compiled code is behaving at run-time consistently with the statically verified code, one can again use paired assert/expect statements with the same expression:
assert _P_;
expect _P_;
The verifier will check that P is always true at the given point in a program
(at the assert
statement).
At run-time, the compiler will insert checks that the same predicate,
in the expect
statement, is true.
Any difference identifies a compiler bug.
Again the expect
must be after the assert
:
if the expect
is first,
then the verifier will interpret the expect
like an assume
,
in which case the assert
will be proved trivially
and potential unsoundness will be hidden.
Using ...
as the argument of the statement is deprecated.
8.20. Reveal Statement (grammar) {#sec-reveal-statement}
Examples:
reveal f(), L;
The reveal
statement makes available to the solver information that is otherwise not visible, as described in the following subsections.
If an assert statement has an expression label, then a proof of that assertion is attempted, but the assertion itself is not used subsequently. For example, consider
method m(i: int) {
assert x: i == 0; // Fails
assert i == 0; // Fails also because the label 'x:' hides the first assertion
}
The first assertion fails. Without the label x:
, the second would succeed because after a failing assertion, the
assertion is assumed in the context of the rest of the program. But with the label, the first assertion is hidden from
the rest of the program. That assertion can be revealed by adding a reveal
statement:
method m(i: int) {
assert x: i == 0; // Fails
reveal x;
assert i == 0; // Now succeeds
}
or
method m(i: int) {
assert x: i == 0; // Fails
assert i == 0 by { reveal x; } // Now succeeds
}
At the point of the reveal
statement, the labeled assertion is made visible and can be used in proving the second assertion.
In this example there is no point to labeling an assertion and then immediately revealing it. More useful are the cases where
the reveal is in an assert-by block or much later in the method body.
In the same way as assertions, preconditions can be labeled.
Within the body of a method, a precondition is an assumption; if the precondition is labeled then that assumption is not visible in the body of the method.
A reveal
statement naming the label of the precondition then makes the assumption visible.
Here is a toy example:
method m(x: int, y: int) returns (z: int)
requires L: 0 < y
ensures z == x+y
ensures x < z
{
z := x + y;
}
The above method will not verify. In particular, the second postcondition cannot be proved.
However, if we add a reveal L;
statement in the body of the method, then the precondition is visible
and both postconditions can be proved.
One could also use this style:
method m(x: int, y: int) returns (z: int)
requires L: 0 < y
ensures z == x+y
ensures x < z
{
z := x + y;
assert x < z by { reveal L; }
}
The reason to possibly hide a precondition is the same as the reason to hide assertions: sometimes less information is better for the solver as it helps the solver focus attention on relevant information.
Section 7 of http://leino.science/papers/krml276.html provides
an extended illustration of this technique to make all the dependencies of an assert
explicit.
By default, function bodies are revealed and available for constructing proofs of assertions that use those functions. However, if a function body is not necessary for a proof, the runtime of the proof can be improved by hiding that body. To do this, use the hide statement. Here's an example:
// We are using the options --isolate-assertions and --type-system-refresh
method Outer(x: int)
requires ComplicatedBody(x)
{
hide ComplicatedBody; // This hides the body of ComplicatedBody for the remainder of the method.
// The body of ComplicatedBody is not needed to prove the requires of Inner
var y := Inner(x);
// We reveal ComplicatedBody inside the following expression, to prove that we are not dividing by zero
var z := (reveal ComplicatedBody; 10 / x);
}
method Inner(x: int) returns (r: int)
requires ComplicatedBody(x)
predicate ComplicatedBody(x: int) {
x != 0 && true // pretend true is complicated
}
Here is a larger example that shows the rules for hide and reveal statements when used on functions:
// We are using the options --isolate-assertions and --type-system-refresh
predicate P() { true }
predicate Q(x: bool) requires x
method Foo() {
var q1 := Q(hide P; P()); // error, precondition not satisfied
var q2 := Q(hide P; reveal P; P()); // no error
hide *;
var q3 := Q(P()); // error, precondition not satisfied
var q4 := Q(reveal P; P()); // no error
if (*) {
reveal P;
assert P();
} else {
assert P(); // error
}
reveal P;
if (*) {
assert P();
} else {
hide *;
assert P(); // error
}
hide *;
if (*) {
reveal P;
} else {
reveal P;
}
assert P(); // error, since the previous two reveal statements are out of scope
}
A const
declaration can be opaque
. If so the value of the constant is not known in reasoning about its uses, just its type and the
fact that the value does not change. The constant's identifier can be listed in a reveal statement. In that case, like other revealed items,
the value of the constant will be known to the reasoning engine until the end of the block containing the reveal statement.
A label or locally declared name in a method body will shadow an opaque constant with the same name outside the method body, making it unable to be revealed without using a qualified name.
8.21. Forall Statement (grammar) {#sec-forall-statement}
Examples:
forall i | 0 <= i < a.Length {
a[i] := 0;
}
forall i | 0 <= i < 100 {
P(i); // P a lemma
}
forall i | 0 <= i < 100
ensures i < 1000 {
}
The forall
statement executes the body
simultaneously for all quantified values in the specified quantifier domain.
You can find more details about quantifier domains here.
There are several variant uses of the forall
statement and there are a number of restrictions.
A forall
statement can be classified as one of the following:
- Assign - the
forall
statement is used for simultaneous assignment. The target must be an array element or an object field. - Call - The body consists of a single call to a ghost method without side effects
- Proof - The
forall
hasensure
expressions which are effectively quantified or proved by the body (if present).
An assign forall
statement performs simultaneous assignment.
The left-hand sides must denote different l-values, unless the
corresponding right-hand sides also coincide.
The following is an excerpt of an example given by Leino in
Developing Verified Programs with Dafny.
When the buffer holding the queue needs to be resized,
the forall
statement is used to simultaneously copy the old contents
into the new buffer.
class SimpleQueue<Data(0)>
{
ghost var Contents: seq<Data>
var a: array<Data> // Buffer holding contents of queue.
var m: int // Index head of queue.
var n: int // Index just past end of queue
method Enqueue(d: Data)
requires a.Length > 0
requires 0 <= m <= n <= a.Length
modifies this, this.a
ensures Contents == old(Contents) + [d]
{
if n == a.Length {
var b := a;
if m == 0 { b := new Data[2 * a.Length]; }
forall i | 0 <= i < n - m {
b[i] := a[m + i];
}
a, m, n := b, 0, n - m;
}
a[n], n, Contents := d, n + 1, Contents + [d];
}
}
Here is an example of a call forall
statement and the
callee. This is contained in the CloudMake-ConsistentBuilds.dfy
test in the Dafny repository.
method m() {
forall cmd', deps', e' |
Hash(Loc(cmd', deps', e')) == Hash(Loc(cmd, deps, e)) {
HashProperty(cmd', deps', e', cmd, deps, e);
}
}
lemma HashProperty(cmd: Expression, deps: Expression, ext: string,
cmd': Expression, deps': Expression, ext': string)
requires Hash(Loc(cmd, deps, ext)) == Hash(Loc(cmd', deps', ext'))
ensures cmd == cmd' && deps == deps' && ext == ext'
The following example of a proof forall
statement comes from the same file:
forall p | p in DomSt(stCombinedC.st) && p in DomSt(stExecC.st)
ensures GetSt(p, stCombinedC.st) == GetSt(p, stExecC.st)
{
assert DomSt(stCombinedC.st) <= DomSt(stExecC.st);
assert stCombinedC.st == Restrict(DomSt(stCombinedC.st),
stExecC.st);
}
More generally, the statement
forall x | P(x) { Lemma(x); }
is used to invoke Lemma(x)
on all x
for which P(x)
holds. If
Lemma(x)
ensures Q(x)
, then the forall statement establishes
forall x :: P(x) ==> Q(x).
The forall
statement is also used extensively in the de-sugared forms of
co-predicates and co-lemmas. See datatypes.
8.22. Modify Statement (grammar) {#sec-modify-statement}
The effect of the modify
statement
is to say that some undetermined
modifications have been made to any or all of the memory
locations specified by the given frame expressions.
In the following example, a value is assigned to field x
followed by a modify
statement that may modify any field
in the object. After that we can no longer prove that the field
x
still has the value we assigned to it. The now unknown values
still are values of their type (e.g. of the subset type or newtype).
class MyClass {
var x: int
method N()
modifies this
{
x := 18;
modify this;
assert x == 18; // error: cannot conclude this here
}
}
Using ...
as the argument of the statement is deprecated.
The form of the modify
statement which includes a block
statement is also deprecated.
The havoc assignment also sets a variable or field to some arbitrary (but type-consistent) value. The difference is that the havoc assignment acts on one LHS variable or memory location; the modify statement acts on all the fields of an object.
8.23. Calc Statement (grammar) {#sec-calc-statement}
See also: Verified Calculations.
The calc
statement supports calculational proofs using a language
feature called program-oriented calculations (poC). This feature was
introduced and explained in the [Verified Calculations] paper by Leino
and Polikarpova[@LEINO:Dafny:Calc]. Please see that paper for a more
complete explanation of the calc
statement. We here mention only the
highlights.
Calculational proofs are proofs by stepwise formula manipulation as is taught in elementary algebra. The typical example is to prove an equality by starting with a left-hand-side and through a series of transformations morph it into the desired right-hand-side.
Non-syntactic rules further restrict hints to only ghost and side-effect free statements, as well as imposing a constraint that only chain-compatible operators can be used together in a calculation. The notion of chain-compatibility is quite intuitive for the operators supported by poC; for example, it is clear that "<" and ">" cannot be used within the same calculation, as there would be no relation to conclude between the first and the last line. See the [paper][Verified Calculations] for a more formal treatment of chain-compatibility.
Note that we allow a single occurrence of the intransitive operator "!=" to
appear in a chain of equalities (that is, "!=" is chain-compatible with
equality but not with any other operator, including itself). Calculations
with fewer than two lines are allowed, but have no effect. If a step
operator is omitted, it defaults to the calculation-wide operator,
defined after the calc
keyword. If that operator is omitted, it defaults
to equality.
Here is an example using calc
statements to prove an elementary
algebraic identity. As it turns out, Dafny is able to prove this without
the calc
statements, but the example illustrates the syntax.
lemma docalc(x : int, y: int)
ensures (x + y) * (x + y) == x * x + 2 * x * y + y * y
{
calc {
(x + y) * (x + y);
==
// distributive law: (a + b) * c == a * c + b * c
x * (x + y) + y * (x + y);
==
// distributive law: a * (b + c) == a * b + a * c
x * x + x * y + y * x + y * y;
==
calc {
y * x;
==
x * y;
}
x * x + x * y + x * y + y * y;
==
calc {
x * y + x * y;
==
// a = 1 * a
1 * x * y + 1 * x * y;
==
// Distributive law
(1 + 1) * x * y;
==
2 * x * y;
}
x * x + 2 * x * y + y * y;
}
}
Here we started with (x + y) * (x + y)
as the left-hand-side
expressions and gradually transformed it using distributive,
commutative and other laws into the desired right-hand-side.
The justification for the steps are given as comments or as
nested calc
statements that prove equality of some sub-parts
of the expression.
The ==
operators show the relation between
the previous expression and the next. Because of the transitivity of
equality we can then conclude that the original left-hand-side is
equal to the final expression.
We can avoid having to supply the relational operator between
every pair of expressions by giving a default operator between
the calc
keyword and the opening brace as shown in this abbreviated
version of the above calc statement:
lemma docalc(x : int, y: int)
ensures (x + y) * (x + y) == x * x + 2 * x * y + y * y
{
calc == {
(x + y) * (x + y);
x * (x + y) + y * (x + y);
x * x + x * y + y * x + y * y;
x * x + x * y + x * y + y * y;
x * x + 2 * x * y + y * y;
}
}
And since equality is the default operator, we could have omitted
it after the calc
keyword.
The purpose of the block statements or the calc
statements between
the expressions is to provide hints to aid Dafny in proving that
step. As shown in the example, comments can also be used to aid
the human reader in cases where Dafny can prove the step automatically.
8.24. Opaque Block (grammar) {#sec-opaque-block}
As a Dafny sequence of statements grows in length, it can become harder to verify later statements in the block. With each statement, new information can become available, and with each modification of the heap, it becomes more expensive to access information from an older heap version. To reduce the verification complexity of long lists of statements, Dafny users can extract part of this block into a separate method or lemma. However, doing so introduces some boilerplate, which is where opaque blocks come in. They achieve a similar effect on verification performance as extracting code, but without the boilerplate.
An opaque block is similar to a block statement: it contains a sequence of zero or more statements, enclosed by curly braces. However, an opaque block is preceded by the keyword 'opaque', and may define ensures and modifies clauses before the curly braces. Anything that happens inside the block is invisible to the statements that come after it, unless it is specified by the ensures clause. Here is an example:
method OpaqueBlockUser() returns (x: int)
ensures x > 4
{
x := 1;
var y := 1;
opaque
ensures x > 3
{
x := x + y;
x := x + 2;
}
assert x == 4; // error
x := x + 1;
}
By default, the modifies clause of an opaque block is the same as that of the enclosing context. Opaque blocks may be nested.
Footnotes
-
The
:-
token is called the elephant symbol or operator. ↩