AutoProof quick start guide
This page gives a short introduction to using AutoProof.
Many things here are simplified in this guide. For a more systematic description please go to the AutoProof tutorial.
What is AutoProof
AutoProof is a static program verifier for Eiffel. It means that AutoProof can take Eiffel programs with contracts and verify that contracts hold in all executions without running the code.
AutoProof generates a model of the program and tries to mathematically prove that contracts hold.
What AutoProof can do?
For example, AutoProof can “see” that the postcondition of the following function holds.
double (a: INTEGER): INTEGER
do
Result := a + a
ensure
is_double: Result = 2 * a
end
AutoProof’s output for this feature is
Successfully verified
as expected.
AutoProof can also “find the bug” in the following implementation:
double (a: INTEGER): INTEGER
do
if a = 7 then
Result := 0
else
Result := a + a
end
ensure
is_double: Result = 2 * a
end
AutoProof’s output for this feature is
Postcondition is_double may be violated
also as expected. Indeed, when the argument
a
is equal
to 7
, the
postcondition does not hold.
Suppose there is a precondition saying that
a
cannot
be 7
. Then
there is no bug and the postcondition “always” holds. More
correctly, the postcondition holds if the precondition held at the
beginning of the routine.
double (a: INTEGER): INTEGER
require
a_is_big: a > 100
do
if a = 7 then
Result := 0
else
Result := a + a
end
ensure
is_double: Result = 2 * a
end
AutoProof’s output is again
Successfully verified
. Indeed, if
a
is
greater than
100
, then
it cannot be
7
, so the
execution cannot enter the
then
branch.
What AutoProof cannot do?
AutoProof cannot invent contracts. If there are no contracts, AutoProof has nothing to verify.
If the program has not enough contracts (underspecification), then AutoProof can say that some check might be violated, while in reality this case cannot happen. Additional preconditions can limit the possible initial state set, which would exclude the impossible states.
Some program mechanisms such as loops, recursion, invariants relying on other objects can be handled by AutoProof but they require extra contracts. Loops are discussed later in this guide. The other mechanisms are out of scope of this guide.
TODO: explain more.
Using AutoProof online
An online version of AutoProof is available at http://autoproof.sit.org/comcom/#AutoProof.
There are several example programs available on that page. You can replace the text of any example with your code.
Click the Verify button to start AutoProof on your code. After AutoProof verification completes, verification results of every feature in the class will be visible in the box under the Verify button.
Notice that the ACCOUNT example does not verify. Indeed, there are bugs in the program. Can you fix the bugs and make all features verify?
Writing Eiffel for AutoProof
Writing code in Eiffel to be verified by AutoProof is not that different from writing normal Eiffel with contracts.
Assertions
check
The
check
instruction can be used to add an assertion which must be verified
by AutoProof at the respective point in the program. Consider the
following example:
check_demo
local
a: INTEGER
do
a := 5
check a_is_five: a = 5 end
check a_is_not_seven: a /= 7 end
a := a + a
check a_is_ten: a = 10 end
check a_is_five_again: a = 5 end -- This fails
end
Verification output is
Check a_is_five_again be violated
. Removing the
last_check
assertion gives the
Successfully verified
result.
Notice that assertion
a = 5
was
checked in two different points of program: the second and the last
line of the routine. The first assertion verified (indeed,
a
is
5
because
it was assigned this value just one line above), the second
assertion did not verify because at that point in the program
a
is in
fact 10
.
Precondition
require
The precondition of a routine is written after the
require
keyword. It can be assumed that the precondition holds before the
first instruction in the body.
The order of precondition clauses is important. The first clause is checked first, then it can be assumed for checking the second clause and so on.
precondition_demo (a: INTEGER)
require
a_is_five_or_ten: a = 5 or a = 10
do
check a_is_small: a < 100 end
end
This feature is successfully verified. Indeed, because of the
precondition
a_is_five_or_ten
we can assume that
a
is
5
or
10
and
both cases are less than
100
, so
the assertion
a_is_small
verifies.
Conditional instruction
if then else
Conditional instruction has a
then
branch and an optional
else
branch. It can be assumed that the condition holds at the beginning
of the
then
branch and the condition does not hold at the beginning of the
else
branch.
conditional_demo (a: INTEGER)
require
a_is_five_or_ten_or_fifteen: a = 5 or a = 10 or a = 15
do
if a = 5 then
check a = 5 end
else
check a /= 5 end
check a = 10 or a = 15 end
if a = 10 then
check a = 10 end
else
check a = 15 end
check a = 10 end -- This fails
end
end
end
Only the last
check a = 10 end
does not verify in this feature. Indeed,
a
could be
one of 5
,
10
,
15
at the
beginning of the body. To reach the failing check
a
must be
not 5
(to
reach the
else
branch) and
a
must be
not 10
(to
reach the next
else
branch).
If a branch is unreachable, its body “is not verified”. You will not get verification errors from the unreachable branches. Consider the following example:
unreachable_demo
do
if 1 = 2 then
check 2 = 1 end
end
end
This feature successfully verifies. There is no such state in which
1 = 2
holds, so there is no possible execution that goes into the
then
branch, so there is no state in which the verifier needs to prove
that
check 2 = 1 end
holds. So, in all possible executions there are no failing checks.
The
check False end
trick can be often used to check if a branch is reachable.
check_false_demo
do
if 1 = 2 then
check first: False end
end
if 1 = 2 or 1 = 1 then
check second: False end
end
end
Only the
second
check fails. The
first
check does not give an error. It hints that the
first
check is not reachable and the
second
check is reachable. Indeed,
1 = 2
does
not hold and
1 = 2 or 1 = 1
holds.
The
check False end
assertion should always fail. If it does not result in a
verification error, then the verifier “thinks” that this line is not
reachable. You can use this trick for debugging. After you have seen
that the branch is reachable, remove the
check False end
assertion.
Postcondition
ensure
The postcondition of a routine is written after the
ensure
keyword. The routine’s body must end in a state where the
postcondition holds, if the precondition held at the beginning of
the routine. In a function, postcondition can use the
Result
variable which is the returned value.
postcondition_demo (a: INTEGER): INTEGER
require
a_is_even: a \\ 2 = 0
do
Result := a + 1
ensure
result_bigger: Result > a
result_is_odd: Result \\ 2 /= 0
end
This feature verifies. If we remove the precondition, then the feature does not verify.
The order of postcondition clauses is important. The first clause is checked after the last line of the body, then it can be assumed for checking the second clause and so on.
Feature calls
f (x)
and
g.f (x)
are opaque
The precondition of the called procedure must hold before the call. After the call, only the postcondition of the procedure can be assumed.
Feature calls are opaque (opposite of transparent). This means that details of the feature body are not visible from outside.
Consider the following function:
more (a: INTEGER): INTEGER
require
a_not_too_big: a < 100
do
Result := a + 1
ensure
result_is_greater: Result > a
end
Even though according to the body this feature returns a number
which is greater exactly by
1
, callers
of this feature do not know this. Callers only see that the result
is greater than the passed value.
call_demo (a: INTEGER): INTEGER
do
if a = 999 then
Result := more (a) -- This fails
end
if a = 10 then
Result := more (a)
check Result > 10 end
check Result = 11 end -- This fails too
end
end
Verification of this feature results in two errors. The first error
is
Precondition a_not_too_big may be violated on call to
{ACCOUNT}.more
in the first branch in which
a
is
999
.
Indeed, calling
more (999)
violates the precondition
a_not_too_big
which says that the argument must be less than
100
.
The second error is
Check Result = 11 may be violated
in the second branch in which
a
is
10
. Notice
that the previous check
Result > 10
verifies. Indeed, caller (call_demo
) sees only the postcondition of
more
which says that the result is simply greater than the passed value,
so
Result > 10
verifies. However, caller does not know the details of the body, so
AutoProof cannot prove the check
Result = 11
.
Do not have inconsistent preconditions
require False
!
Be careful not to have an inconstistent precondition. If the routine’s precondition is not satisfiable, this routine will be Successfully verified irrespective of its body or postcondition. Most probably you do not want this!
AutoProof verifies that for all initial states satisfying the precondition, postconditions and intermediate checks are satisfied. If there are no such initial states, the feature is successfully verified.
The most simple inconsistent precondition is just
False
:
simply_false: False
Contradicting precondition clauses also result in an inconsistent precondition:
a_positive: a > 0
a_negative: a < 0
These are just several examples of inconsistent preconditions. Many more are possible depending on the surrounding program.
There are several ways to catch inconsistent preconditions.
Calling a feature with an inconsistent precondition will not verify. Indeed, there will be no state that satisfies the inconsistent precondition. So, you can write a test feature that calls your feature. If your test feature verifies, it will be a proof of having at least several states that satisfy the precondition.
Adding a
check False end
at the beginning of the feature body. Verifying
check False end
should fail. If it does not fail, it means that execution cannot
reach this line. If the execution cannot reach the first line, it
might mean that the precondition is inconsistent. After you have
checked the precondition and
check False end
gives an error, you can remove it to verify the rest of the routine.
These checks are not completely safe. However, they are quick ways to find some inconsistencies using the verifier.
Loops
from invariant until loop variant
Verifying loops has some important differences when compared with how they are executed.
Loops in Eiffel have the
from
initialization part which can contain initialization instructions,
the
invariant
part which can contain loop invariant assertions, the
until
part
which must contain one boolean expression which is the exit
condition, the
variant
part which must contain one integer expression which is the loop
variant.
As a reminder, loop variant is an integer expression that must decrease every iteration but cannot become less than zero. The presence of such an invariant proves that the loop terminates, because an integer value cannot decrease infinitely and must eventually stop at zero or earlier.
When loops are executed, each next iteration starts from the state that achieved at the end of the previous iteration. The first iteration executes from the state that happened before the loop.
Verification uses a simplified model of loop execution to simplify the verifier’s job. This is a technical limitation. Consider the following loop:
from
invariant
loop_invariant
until
exit_condition
loop
loop_body
variant
loop_variant
end
Each iteration is verified starting in the same abstract state which is represented by the loop invariant. This way, loop iteration can assume the loop invariant and must ensure loop invariant at the end. Additionally, iteration can assume that the exit condition does not hold. In other words, we can say that the loop body is verified similarly to the following procedure:
one_loop_iteration
require
invariant_assumed: loop_invariant
loop_not_terminated: not exit_condition
do
loop_body
ensure
invariant_preserved: loop_invariant
variant_decreased: loop_variant < old loop_variant
variant_non_neg: loop_variant >= 0
end
This imaginary feature highlights the important aspects of loop
verification. The loop body can rely on the invariant and can assume
that exit condition does not hold. Loop body must ensure loop
invariant (even in the last iteration!). The loop body can do
anything with the exit condition: keep it
False
or
make it
True
. The
loop body must decrease the variant but must not make it negative.
Loops are opaque similarly to the feature calls.
whole_loop_application
require
loop_invariant
ensure
loop_invariant
exit_condition
end
This imaginary feature shows the pre- and post-condition of a loop as a whole. Loop application requires the invariant and ensures the invariant and the exit condition. Loop invariant must hold even if the exit condition holds before the first iteration.
FAQ and common problems
Invariant might not hold on call to
{ANY}.wrap
The error
Invariant might not hold on call to {ANY}.wrap
means that AutoProof thinks that the invariant of the current object
must hold at this line of the program.
If the error’s line number points to the
end
keyword of the feature, it means that the invariant must hold at the
end of the body of the feature.
Further reading
To continue learning about AutoProof, please go to the AutoProof tutorial. The tutorial provides much more detail on the topics already covered here and talks about advanced features of AutoProof.