Introduction To SPARK
Introduction To SPARK
Release 2022-06
Claire Dross
and Yannick Moy
1 SPARK Overview 3
1.1 What is it? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 What do the tools do? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3 Key Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.4 A trivial example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.5 The Programming Language . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.6 Limitations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.6.1 No side-effects in expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.6.2 No aliasing of names . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.7 Designating SPARK Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.8 Code Examples / Pitfalls . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.8.1 Example #1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.8.2 Example #2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.8.3 Example #3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.8.4 Example #4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.8.5 Example #5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.8.6 Example #6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.8.7 Example #7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.8.8 Example #8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.8.9 Example #9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.8.10 Example #10 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2 Flow Analysis 21
2.1 What does flow analysis do? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.2 Errors Detected . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.2.1 Uninitialized Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.2.2 Ineffective Statements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.2.3 Incorrect Parameter Mode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.3 Additional Verifications . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.3.1 Global Contracts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.3.2 Depends Contracts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.4 Shortcomings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.4.1 Modularity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.4.2 Composite Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.4.3 Value Dependency . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.4.4 Contract Computation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.5 Code Examples / Pitfalls . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.5.1 Example #1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.5.2 Example #2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.5.3 Example #3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.5.4 Example #4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
2.5.5 Example #5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
2.5.6 Example #6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
2.5.7 Example #7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
i
2.5.8 Example #8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2.5.9 Example #9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
2.5.10 Example #10 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
4 State Abstraction 71
4.1 What's an Abstraction? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
4.2 Why is Abstraction Useful? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
4.3 Abstraction of a Package's State . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
4.4 Declaring a State Abstraction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
4.5 Refining an Abstract State . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
4.6 Representing Private Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
4.7 Additional State . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
4.7.1 Nested Packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
4.7.2 Constants that Depend on Variables . . . . . . . . . . . . . . . . . . . . . . . . . 77
4.8 Subprogram Contracts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
4.8.1 Global and Depends . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
4.8.2 Preconditions and Postconditions . . . . . . . . . . . . . . . . . . . . . . . . . . 80
4.9 Initialization of Local Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
4.10 Code Examples / Pitfalls . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
4.10.1 Example #1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
4.10.2 Example #2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
4.10.3 Example #3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
4.10.4 Example #4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
4.10.5 Example #5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
4.10.6 Example #6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
4.10.7 Example #7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
4.10.8 Example #8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
4.10.9 Example #9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
4.10.10 Example #10 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
ii
5.3 Guide Proof . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
5.3.1 Local Ghost Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
5.3.2 Ghost Procedures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
5.3.3 Handling of Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
5.3.4 Loop Invariants . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
5.4 Code Examples / Pitfalls . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
5.4.1 Example #1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
5.4.2 Example #2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
5.4.3 Example #3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
5.4.4 Example #4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
5.4.5 Example #5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
5.4.6 Example #6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
5.4.7 Example #7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
5.4.8 Example #8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
5.4.9 Example #9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
5.4.10 Example #10 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
iii
iv
Introduction to SPARK
This tutorial is an interactive introduction to the SPARK programming language and its formal ver-
ification tools. You will learn the difference between Ada and SPARK and how to use the various
analysis tools that come with SPARK.
This document was prepared by Claire Dross and Yannick Moy.
1 http://creativecommons.org/licenses/by-sa/4.0
CONTENTS: 1
Introduction to SPARK
2 CONTENTS:
CHAPTER
ONE
SPARK OVERVIEW
This tutorial is an introduction to the SPARK programming language and its formal verification tools.
You need not know any specific programming language (although going over the Introduction to
Ada course first may help) or have experience in formal verification.
Version 2012 of Ada introduced the use of aspects, which can be used for subprogram contracts,
and version 2014 of SPARK added its own aspects to further aid static analysis.
3
Introduction to SPARK
We start by reviewing static verification of programs, which is verification of the source code per-
formed without compiling or executing it. Verification uses tools that perform static analysis. These
can take various forms. They include tools that check types and enforce visibility rules, such as the
compiler, in addition to those that perform more complex reasoning, such as abstract interpreta-
tion, as done by a tool like CodePeer2 from AdaCore. The tools that come with SPARK perform two
different forms of static analysis:
• flow analysis is the fastest form of analysis. It checks initializations of variables and looks
at data dependencies between inputs and outputs of subprograms. It can also find unused
assignments and unmodified variables.
• proof checks for the absence of runtime errors as well as the conformance of the program
with its specifications.
The tool for formal verification of the SPARK language is called GNATprove. It checks for confor-
mance with the SPARK subset and performs flow analysis and proof of the source code. Several
other tools support the SPARK language, including both the GNAT compiler3 and the GNAT Studio
integrated development environment4 .
We start with a simple example of a subprogram in Ada that uses SPARK aspects to specify verifiable
subprogram contracts. The subprogram, called Increment, adds 1 to the value of its parameter
X:
Listing 1: increment.ads
1 procedure Increment
2 (X : in out Integer)
3 with
4 Global => null,
5 Depends => (X => X),
6 Pre => X < Integer'Last,
7 Post => X = X'Old + 1;
Listing 2: increment.adb
1 procedure Increment
2 (X : in out Integer)
3 is
4 begin
5 X := X + 1;
6 end Increment;
Prover output
2 https://www.adacore.com/codepeer
3 https://www.adacore.com/gnatpro
4 https://www.adacore.com/gnatpro/toolsuite/gps
The contracts are written using the Ada aspect feature and those shown specify several properties
of this subprogram:
• The SPARK Global aspect says that Increment does not read or write any global variables.
• The SPARK Depend aspect is especially interesting for security: it says that the value of the
parameter X after the call depends only on the (previous) value of X.
• The Pre and Post aspects of Ada specify functional properties of Increment:
– Increment is only allowed to be called if the value of X prior to the call is less than
Integer'Last. This ensures that the addition operation performed in the subprogram
body doesn't overflow.
– Increment does indeed perform an increment of X: the value of X after a call is one
greater than its value before the call.
GNATprove can verify all of these contracts. In addition, it verifies that no error can be raised at
runtime when executing Increment's body.
It's important to understand why there are differences between the SPARK and Ada languages. The
aim when designing the SPARK subset of Ada was to create the largest possible subset of Ada that
was still amenable to simple specification and sound verification.
The most notable restrictions from Ada are related to exceptions and access types, both of which
are known to considerably increase the amount of user-written annotations required for full sup-
port. Backwards goto statements and controlled types are also not supported since they introduce
non-trivial control flow. The two remaining restrictions relate to side-effects in expressions and
aliasing of names, which we now cover in more detail.
1.6 Limitations
The SPARK language doesn't allow side-effects in expressions. In other words, evaluating a SPARK
expression must not update any object. This limitation is necessary to avoid unpredictable behav-
ior that depends on order of evaluation, parameter passing mechanisms, or compiler optimiza-
tions. The expression for Dummy below is non-deterministic due to the order in which the two calls
to F are evaluated. It's therefore not legal SPARK.
Listing 3: show_illegal_ada_code.adb
1 procedure Show_Illegal_Ada_Code is
2
10 Dummy : Integer := 0;
11
12 begin
13 Dummy := F (Dummy) - F (Dummy); -- ??
14 end Show_Illegal_Ada_Code;
Build output
Prover output
In fact, the code above is not even legal Ada, so the same error is generated by the GNAT compiler.
But SPARK goes further and GNATprove also produces an error for the following equivalent code
that is accepted by the Ada compiler:
Listing 4: show_illegal_spark_code.adb
1 procedure Show_Illegal_SPARK_Code is
2
3 Dummy : Integer := 0;
4
12 begin
13 Dummy := F - F; -- ??
14 end Show_Illegal_SPARK_Code;
Prover output
The SPARK languages enforces the lack of side-effects in expressions by forbidding side-effects in
functions, which include modifications to either parameters or global variables. As a consequence,
SPARK forbids functions with out or in out parameters in addition to functions modifying a global
variable. Function F below is illegal in SPARK, while Function Incr might be legal if it doesn't modify
any global variables and function Incr_And_Log might be illegal if it modifies global variables to
perform logging.
In most cases, you can easily replace these functions by procedures with an out parameter that
returns the computed value.
When it has access to function bodies, GNATprove verifies that those functions are indeed free
from side-effects. Here for example, the two functions Incr and Incr_And_Log have the same
signature, but only Incr is legal in SPARK. Incr_And_Log isn't: it attempts to update the global
variable Call_Count.
Listing 5: side_effects.ads
1 package Side_Effects is
2
7 end Side_Effects;
Listing 6: side_effects.adb
1 package body Side_Effects is
2
6 Call_Count : Natural := 0;
7
14 end Side_Effects;
Prover output
Another restriction imposed by the SPARK subset concerns aliasing5 . We say that two names are
aliased if they refer to the same object. There are two reasons why aliasing is forbidden in SPARK:
• It makes verification more difficult because it requires taking into account the fact that mod-
ifications to variables with different names may actually update the same object.
• Results may seem unexpected from a user point of view. The results of a subprogram call
may depend on compiler-specific attributes, such as parameter passing mechanisms, when
5 https://en.wikipedia.org/wiki/Aliasing_(computing)
1.6. Limitations 7
Introduction to SPARK
Listing 7: no_aliasing.adb
1 procedure No_Aliasing is
2
3 Total : Natural := 0;
4
13 X : Natural := 3;
14
15 begin
16 Move_To_Total (X); -- OK
17 pragma Assert (Total = 3); -- OK
18 Move_To_Total (Total); -- flow analysis error
19 pragma Assert (Total = 6); -- runtime error
20 end No_Aliasing;
Prover output
Runtime output
Move_To_Total adds the value of its input parameter Source to the global variable Total and
then resets Source to 0. The programmer has clearly not taken into account the possibility of an
aliasing between Total and Source. (This sort of error is quite common.)
This procedure itself is valid SPARK. When doing verification, GNATprove assumes, like the pro-
grammer did, that there's no aliasing between Total and Source. To ensure this assumption is
valid, GNATprove checks for possible aliasing on every call to Move_To_Total. Its final call in pro-
cedure No_Aliasing violates this assumption, which produces both a message from GNATprove
and a runtime error (an assertion violation corresponding to the expected change in Total from
calling Move_To_Total). Note that the postcondition of Move_To_Total is not violated on this
second call since integer parameters are passed by copy and the postcondition is checked before
the copy-back from the formal parameters to the actual arguments.
Aliasing can also occur as a result of using access types (pointers6 in Ada). These are restricted in
SPARK so that only benign aliasing is allowed, when both names are only used to read the data. In
6 https://en.m.wikipedia.org/wiki/Pointer_(computer_programming)
particular, assignment between access objects operates a transfer of ownership, where the source
object loses its permission to read or write the underlying allocated memory.
Procedure Ownership_Transfer is an example of code that is legal in Ada but rejected in SPARK
due to aliasing:
Listing 8: ownership_transfer.adb
1 procedure Ownership_Transfer is
2 type Int_Ptr is access Integer;
3 X : Int_Ptr;
4 Y : Int_Ptr;
5 Dummy : Integer;
6 begin
7 X := new Integer'(1);
8 X.all := X.all + 1;
9 Y := X;
10 Y.all := Y.all + 1;
11 X.all := X.all + 1; -- illegal
12 X.all := 1; -- illegal
13 Dummy := X.all; -- illegal
14 end Ownership_Transfer;
Prover output
After the assignment of X to Y, variable X cannot be used anymore to read or write the underlying
allocated memory.
Note: For more details on these limitations, see the SPARK User's Guide7 .
Since the SPARK language is restricted to only allow easily specifiable and verifiable constructs,
there are times when you can't or don't want to abide by these limitations over your entire code
base. Therefore, the SPARK tools only check conformance to the SPARK subset on code which you
identify as being in SPARK.
You do this by using an aspect named SPARK_Mode. If you don't explicitly specify otherwise,
SPARK_Mode is Off, meaning you can use the complete set of Ada features in that code and that
it should not be analyzed by GNATprove. You can change this default either selectively (on some
units or subprograms or packages inside units) or globally (using a configuration pragma, which is
what we're doing in this tutorial). To allow simple reuse of existing Ada libraries, entities declared
in imported units with no explicit SPARK_Mode can still be used from SPARK code. The tool only
7 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/language_restrictions.html#
language-restrictions
checks for SPARK conformance on the declaration of those entities which are actually used within
the SPARK code.
Here's a common case of using the SPARK_Mode aspect:
package P
with SPARK_Mode => On
is
-- package spec is IN SPARK, so can be used by SPARK clients
end P;
package body P
with SPARK_Mode => Off
is
-- body is NOT IN SPARK, so is ignored by GNATprove
end P;
The package P only defines entities whose specifications are in the SPARK subset. However, it
wants to use all Ada features in its body. Therefore the body should not be analyzed and has its
SPARK_Mode aspect set to Off.
You can specify SPARK_Mode in a fine-grained manner on a per-unit basis. An Ada package has
four different components: the visible and private parts of its specification and the declarative and
statement parts of its body. You can specify SPARK_Mode as being either On or Off on any of those
parts. Likewise, a subprogram has two parts: its specification and its body.
A general rule in SPARK is that once SPARK_Mode has been set to Off, it can never be switched On
again in the same part of a package or subprogram. This prevents setting SPARK_Mode to On for
subunits of a unit with SPARK_Mode Off and switching back to SPARK_Mode On for a part of a given
unit where it was set fo Off in a previous part.
Note: For more details on the use of SPARK_Mode, see the SPARK User's Guide8 .
1.8.1 Example #1
Here's a package defining an abstract stack type (defined as a private type in SPARK) of Element
objects along with some subprograms providing the usual functionalities of stacks. It's marked as
being in the SPARK subset.
Listing 9: stack_package.ads
1 package Stack_Package
2 with SPARK_Mode => On
3 is
4 type Element is new Natural;
5 type Stack is private;
6
11 private
12 type Stack is record
13 Top : Integer;
(continues on next page)
8 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/spark_mode.html#
17 end Stack_Package;
Prover output
Side-effects in expressions are not allowed in SPARK. Therefore, Pop is not allowed to modify its
parameter S.
1.8.2 Example #2
Let's turn to an abstract state machine version of a stack, where the unit provides a single instance
of a stack. The content of the stack (global variables Content and Top) is not directly visible to
clients. In this stripped-down version, only the function Pop is available to clients. The package
spec and body are marked as being in the SPARK subset.
8 end Global_Stack;
7 Content : Element_Array;
8 Top : Natural;
9
17 end Global_Stack;
Prover output
global_stack.ads:6:13: error: function with output global "Top" is not allowed in␣
↪SPARK
As above, functions should be free from side-effects. Here, Pop updates the global variable Top,
which is not allowed in SPARK.
1.8.3 Example #3
We now consider two procedures: Permute and Swap. Permute applies a circular permutation to
the value of its three parameters. Swap then uses Permute to swap the value of X and Y.
3 procedure Test_Swap
4 with SPARK_Mode => On
5 is
6 A : Integer := 1;
7 B : Integer := 2;
8 begin
9 Swap (A, B);
10 end Test_Swap;
Build output
p.adb:14:19: error: writable actual for "Y" overlaps with actual for "Z"
gprbuild: *** compilation phase failed
Prover output
Here, the values for parameters Y and Z are aliased in the call to Permute, which is not allowed
in SPARK. In fact, in this particular case, this is even a violation of Ada rules so the same error is
issued by the Ada compiler.
In this example, we see the reason why aliasing is not allowed in SPARK: since Y and Z are Positive,
they are passed by copy and the result of the call to Permute depends on the order in which they're
copied back after the call.
1.8.4 Example #4
Here, the Swap procedure is used to swap the value of the two record components of R.
16 end P;
Prover output
This code is correct. The call to Swap is safe: two different components of the same record can't
refer to the same object.
1.8.5 Example #5
Here's a slight modification of the previous example using an array instead of a record:
Swap_Indexes calls Swap on values stored in the array A.
16 end P;
Prover output
GNATprove detects a possible case of aliasing. Unlike the previous example, it has no way of know-
ing that the two elements A (I) and A (J) are actually distinct when we call Swap. GNATprove
issues a check message here instead of an error, giving you the possibility of justifying the message
after review (meaning that you've verified manually that this can't, in fact, occur).
1.8.6 Example #6
We now consider a package declaring a type Dictionary, an array containing a word per letter.
The procedure Store allows us to insert a word at the correct index in a dictionary.
3 package P
4 with SPARK_Mode => On
5 is
6 subtype Letter is Character range 'a' .. 'z';
7 type String_Access is new Ada.Finalization.Controlled with record
8 Ptr : access String;
9 end record;
10 type Dictionary is array (Letter) of String_Access;
11
Prover output
This code is not correct: controlled types are not part of the SPARK subset. The solution here is to
use SPARK_Mode to separate the definition of String_Access from the rest of the code in a fine
grained manner.
1.8.7 Example #7
Here's a new version of the previous example, which we've modified to hide the controlled type
inside the private part of package P, using pragma SPARK_Mode (Off) at the start of the private
part.
3 package P
4 with SPARK_Mode => On
5 is
6 subtype Letter is Character range 'a' .. 'z';
7 type String_Access is private;
8 type Dictionary is array (Letter) of String_Access;
9
14 private
15 pragma SPARK_Mode (Off);
16
Prover output
Since the controlled type is defined and used inside of a part of the code ignored by GNATprove,
this code is correct.
1.8.8 Example #8
Let's put together the new spec for package P with the body of P seen previously.
3 package P
4 with SPARK_Mode => On
5 is
6 subtype Letter is Character range 'a' .. 'z';
7 type String_Access is private;
8 type Dictionary is array (Letter) of String_Access;
9
14 private
(continues on next page)
Prover output
Phase 1 of 2: generation of Global contracts ...
p.adb:1:01: error: incorrect application of SPARK_Mode at /vagrant/frontend/dist/
↪test_output/projects/Courses/Intro_To_Spark/Overview/Example_08/main.adc:12
p.adb:1:01: error: value Off was set for SPARK_Mode on "P" at p.ads:15
p.adb:2:08: error: incorrect use of SPARK_Mode
p.adb:2:08: error: value Off was set for SPARK_Mode on "P" at p.ads:15
gnatprove: error during generation of Global contracts
The body of Store doesn't actually use any construct that's not in the SPARK subset, but we nev-
ertheless can't set SPARK_Mode to On for P's body because it has visibility to P's private part, which
is not in SPARK, even if we don't use it.
1.8.9 Example #9
Next, we moved the declaration and the body of the procedure Store to another package named
Q.
3 package P
4 with SPARK_Mode => On
5 is
6 subtype Letter is Character range 'a' .. 'z';
7 type String_Access is private;
8 type Dictionary is array (Letter) of String_Access;
9
12 private
13 pragma SPARK_Mode (Off);
14
Prover output
And now everything is fine: we've managed to retain the use of the controlled type while having
most of our code in the SPARK subset so GNATprove is able to analyze it.
Our final example is a package with two functions to search for the value 0 inside an array A. The
first raises an exception if 0 isn't found in A while the other simply returns 0 in that case.
Prover output
This code is perfectly correct, despite the use of exception handling, because we've carefully
isolated this non-SPARK feature in a function body marked with a SPARK_Mode of Off so it's
ignored by GNATprove. However, GNATprove tries to show that Not_Found is never raised
in Search_Zero_P, producing a message about a possible exception being raised. Looking at
Search_Zero_N, it's indeed likely that an exception is meant to be raised in some cases, which
means you need to verify that Not_Found is only raised when appropriate using other methods
such as peer review or testing.
TWO
FLOW ANALYSIS
In this section we present the flow analysis capability provided by the GNATprove tool, a critical
tool for using SPARK.
Flow analysis concentrates primarily on variables. It models how information flows through them
during a subprogram's execution, connecting the final values of variables to their initial values. It
analyzes global variables declared at library level, local variables, and formal parameters of sub-
programs.
Nesting of subprograms creates what we call scope variables: variables declared locally to an en-
closing unit. From the perspective of a nested subprogram, scope variables look very much like
global variables
Flow analysis is usually fast, roughly as fast as compilation. It detects various types of errors and
finds violations of some SPARK legality rules, such as the absence of aliasing and freedom of ex-
pressions from side-effects. We discussed these rules in the SPARK Overview (page 3).
Flow analysis is sound: if it doesn't detect any errors of a type it's supposed to detect, we know for
sure there are no such errors.
We now present each class of errors detected by flow analysis. The first is the reading of an unini-
tialized variable. This is nearly always an error: it introduces non-determinism and breaks the type
system because the value of an uninitialized variable may be outside the range of its subtype. For
these reasons, SPARK requires every variable to be initialized before being read.
Flow analysis is responsible for ensuring that SPARK code always fulfills this requirement. For ex-
ample, in the function Max_Array shown below, we've neglected to initialize the value of Max prior
to entering the loop. As a consequence, the value read by the condition of the if statement may be
uninitialized. Flow analysis detects and reports this error.
Listing 1: show_uninitialized.ads
1 package Show_Uninitialized is
2
21
Introduction to SPARK
7 end Show_Uninitialized;
Listing 2: show_uninitialized.adb
1 package body Show_Uninitialized is
2
14 end Show_Uninitialized;
Prover output
Note: For more details on how flow analysis verifies data initialization, see the SPARK User's
Guide9 .
Ineffective statements are different than dead code: they're executed, and often even modify the
value of variables, but have no effect on any of the subprogram's visible outputs: parameters,
global variables or the function result. Ineffective statements should be avoided because they
make the code less readable and more difficult to maintain.
More importantly, they're often caused by errors in the program: the statement may have been
written for some purpose, but isn't accomplishing that purpose. These kinds of errors can be dif-
ficult to detect in other ways.
For example, the subprograms Swap1 and Swap2 shown below don't properly swap their two pa-
rameters X and Y. This error caused a statement to be ineffective. That ineffective statement is not
an error in itself, but flow analysis produces a warning since it can be indicative of an error, as it is
here.
Listing 3: show_ineffective_statements.ads
1 package Show_Ineffective_Statements is
2
data-initialization-policy
8 end Show_Ineffective_Statements;
Listing 4: show_ineffective_statements.adb
1 package body Show_Ineffective_Statements is
2
11 Tmp : T := 0;
12
20 end Show_Ineffective_Statements;
Prover output
So far, we've seen examples where flow analysis warns about ineffective statements and unused
variables.
Parameter modes are an important part of documenting the usage of a subprogram and affect the
code generated for that subprogram. Flow analysis checks that each specified parameter mode
corresponds to the usage of that parameter in the subprogram's body. It checks that an in param-
eter is never modified, either directly or through a subprogram call, checks that the initial value of
an out parameter is never read in the subprogram (since it may not be defined on subprogram
entry), and warns when an in out parameter isn't modified or when its initial value isn't used. All
of these may be signs of an error.
We see an example below. The subprogram Swap is incorrect and GNATprove warns about an
Listing 5: show_incorrect_param_mode.ads
1 package Show_Incorrect_Param_Mode is
2
7 end Show_Incorrect_Param_Mode;
Listing 6: show_incorrect_param_mode.adb
1 package body Show_Incorrect_Param_Mode is
2
10 end Show_Incorrect_Param_Mode;
Prover output
In SPARK, unlike Ada, you should declare an out parameter to be in out if it's not modified on
every path, in which case its value may depend on its initial value. SPARK is stricter than Ada to
allow more static detection of errors. This table summarizes SPARK's valid parameter modes as a
function of whether reads and writes are done to the parameter.
Initial value read Written on some path Written on every path Parameter mode
X in
X X in out
X X in out
X in out
X out
So far, none of the verifications we've seen require you to write any additional annotations. How-
ever, flow analysis also checks flow annotations that you write. In SPARK, you can specify the set of
global and scoped variables accessed or modified by a subprogram. You do this using a contract
named Global.
When you specify a Global contract for a subprogram, flow analysis checks that it's both correct
and complete, meaning that no variables other than those stated in the contract are accessed
or modified, either directly or through a subprogram call, and that all those listed are accessed
or modified. For example, we may want to specify that the function Get_Value_Of_X reads the
value of the global variable X and doesn't access any other global variable. If we do this through
a comment, as is usually done in other languages, GNATprove can't verify that the code complies
with this specification:
package Show_Global_Contracts is
X : Natural := 0;
end Show_Global_Contracts;
You write global contracts as part of the subprogram specification. In addition to their value in flow
analysis, they also provide useful information to users of a subprogram. The value you specify for
the Global aspect is an aggregate-like list of global variable names, grouped together according
to their mode.
In the example below, the procedure Set_X_To_Y_Plus_Z reads both Y and Z. We indicate this
by specifying them as the value for Input. It also writes X, which we specify using Output. Since
Set_X_To_X_Plus_Y both writes X and reads its initial value, X's mode is In_Out. Like parame-
ters, if no mode is specified in a Global aspect, the default is Input. We see this in the case of the
declaration of Get_Value_Of_X. Finally, if a subprogram, such as Incr_Parameter_X, doesn't
reference any global variables, you set the value of the global contract to null.
Listing 7: show_global_contracts.ads
1 package Show_Global_Contracts is
2
3 X, Y, Z : Natural := 0;
4
20 end Show_Global_Contracts;
Prover output
Note: For more details on global contracts, see the SPARK User's Guide10 .
10 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/subprogram_contracts.html#
data-dependencies
You may also supply a Depends contract for a subprogram to specify dependencies between its
inputs and outputs. These dependencies include not only global variables but also parameters and
the function's result. When you supply a Depends contract for a subprogram, flow analysis checks
that it's correct and complete, that is, for each dependency you list, the variable depends on those
listed and on no others.
For example, you may want to say that the new value of each parameter of Swap, shown below,
depends only on the initial value of the other parameter and that the value of X after the return of
Set_X_To_Zero doesn't depend on any global variables. If you indicate this through a comment,
as you often do in other languages, GNATprove can't verify that this is actually the case.
package Show_Depends_Contracts is
X : Natural;
procedure Set_X_To_Zero;
-- The value of X after the call depends on no input
end Show_Depends_Contracts;
Like Global contracts, you specify a Depends contract in subprogram declarations using an as-
pect. Its value is a list of one or more dependency relations between the outputs and inputs of the
subprogram. Each relation is represented as two lists of variable names separated by an arrow. On
the left of each arrow are variables whose final value depends on the initial value of the variables
you list on the right.
For example, here we indicate that the final value of each parameter of Swap depends only on the
initial value of the other parameter. If the subprogram is a function, we list its result as an output,
using the Result attribute, as we do for Get_Value_Of_X below.
Listing 8: show_depends_contracts.ads
1 package Show_Depends_Contracts is
2
5 X, Y, Z : T := 0;
6
33 end Show_Depends_Contracts;
Prover output
Often, the final value of a variable depends on its own initial value. You can specify this in a concise
way using the + character, as we did in the specification of Set_X_To_X_Plus_Y above. If there's
more than one variable on the left of the arrow, a + means each variables depends on itself, not
that they all depend on each other. You can write the corresponding dependency with (=> +) or
without (=>+) whitespace.
If you have a program where an input isn't used to compute the final value of any output, you ex-
press that by writting null on the left of the dependency relation, as we did for the Do_Nothing
subprogram above. You can only write one such dependency relation, which lists all unused in-
puts of the subprogram, and it must be written last. Such an annotation also silences flow analysis'
warning about unused parameters. You can also write null on the right of a dependency rela-
tion to indicate that an output doesn't depend on any input. We do that above for the procedure
Set_X_To_Zero.
Note: For more details on depends contracts, see the SPARK User's Guide11 .
2.4 Shortcomings
2.4.1 Modularity
Flow analysis is sound, meaning that if it doesn't output a message on some analyzed SPARK code,
you can be assured that none of the errors it tests for can occur in that code. On the other hand,
flow analysis often issues messages when there are, in fact, no errors. The first, and probably most
common reason for this relates to modularity.
To scale flow analysis to large projects, verifications are usually done on a per-subprogram ba-
sis, including detection of uninitialized variables. To analyze this modularly, flow analysis needs to
assume the initialization of inputs on subprogram entry and modification of outputs during sub-
program execution. Therefore, each time a subprogram is called, flow analysis checks that global
and parameter inputs are initialized and each time a subprogram returns, it checks that global and
parameter outputs were modified.
This can produce error messages on perfectly correct subprograms. An example is
Set_X_To_Y_Plus_Z below, which only sets its out parameter X when Overflow is False.
11 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/subprogram_contracts.html#
flow-dependencies
2.4. Shortcomings 27
Introduction to SPARK
Listing 9: set_x_to_y_plus_z.adb
1 procedure Set_X_To_Y_Plus_Z
2 (Y, Z : Natural;
3 X : out Natural;
4 Overflow : out Boolean)
5 is
6 begin
7 if Natural'Last - Z < Y then
8 Overflow := True; -- X should be initialized on every path
9 else
10 Overflow := False;
11 X := Y + Z;
12 end if;
13 end Set_X_To_Y_Plus_Z;
Prover output
The message means that flow analysis wasn't able to verify that the program didn't read an unini-
tialized variable. To solve this problem, you can either set X to a dummy value when there's an
overflow or manually verify that X is never used after a call to Set_X_To_Y_Plus_Z that returned
True as the value of Overflow.
Another common cause of false alarms is caused by the way flow analysis handles composite types.
Let's start with arrays.
Flow analysis treats an entire array as single object instead of one object per element, so it con-
siders modifying a single element to be a modification of the array as a whole. Obviously, this
makes reasoning about which global variables are accessed less precise and hence the dependen-
cies of those variables are also less precise. This also affects the ability to accurately detect reads
of uninitialized data.
It's sometimes impossible for flow analysis to determine if an entire array object has been initial-
ized. For example, after we write code to initialize every element of an unconstrained array A in
chunks, we may still receive a message from flow analysis claiming that the array isn't initialized. To
resolve this issue, you can either use a simpler loop over the full range of the array, or (even better)
an aggregate assignment, or, if that's not possible, verify initialization of the object manually.
9 end Show_Composite_Types_Shortcoming;
26 end Show_Composite_Types_Shortcoming;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: analysis of data and information flow ...
show_composite_types_shortcoming.ads:5:27: medium: "A" might not be initialized in
↪"Init_Chunks"
Flow analysis is more precise on record objects because it tracks the value of each component of a
record separately within a single subprogram. So when a record object is initialized by successive
assignments of its components, flow analysis knows that the entire object is initialized. However,
record objects are still treated as single objects when analyzed as an input or output of a subpro-
gram.
10 end Show_Record_Flow_Analysis;
2.4. Shortcomings 29
Introduction to SPARK
10 end Show_Record_Flow_Analysis;
Prover output
Flow analysis complains when a procedure call initializes only some components of a record object.
It'll notify you of uninitialized components, as we see in subprogram Init_F2 below.
11 end Show_Record_Flow_Analysis;
3 procedure Init_F2
4 (R : in out Rec) is
5 begin
6 R.F2 := 0;
7 end Init_F2;
8
15 end Show_Record_Flow_Analysis;
Prover output
Flow analysis is not value-dependent: it never reasons about the values of expressions, only
whether they have been set to some value or not. As a consequence, if some execution path in a
subprogram is impossible, but the impossibility can only be determined by looking at the values
of expressions, flow analysis still considers that path feasible and may emit messages based on it
believing that execution along such a path is possible.
For example, in the version of Absolute_Value below, flow analysis computes that R is uninitial-
ized on a path that enters neither of the two conditional statements. Because it doesn't consider
values of expressions, it can't know that such a path is impossible.
Prover output
To avoid this problem, you should make the control flow explicit, as in this second version of Ab-
solute_Value:
2.4. Shortcomings 31
Introduction to SPARK
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: analysis of data and information flow ...
The final cause of unexpected flow messages that we'll discuss also comes from inaccuracy in com-
putations of contracts. As we explained earlier, both Global and Depends contracts are optional,
but GNATprove uses their data for some of its analysis.
For example, flow analysis can't detect reads from uninitialized variables without knowing the set
of variables accessed. It needs to analyze and check both the Depends contracts you wrote for
a subprogram and those you wrote for callers of that subprogram. Since each flow contract on
a subprogram depends on the flow contracts of all the subprograms called inside its body, this
computation can often be quite time-consuming. Therefore, flow analysis sometimes trades-off
the precision of this computation against the time a more precise computation would take.
This is the case for Depends contracts, where flow analysis simply assumes the worst, that each
subprogram's output depends on all of that subprogram's inputs. To avoid this assumption, all
you have to do is supply contracts when default ones are not precise enough. You may also want
to supply Global contracts to further speed up flow analysis on larger programs.
2.5.1 Example #1
5 procedure Search_Array
6 (A : Array_Of_Positives;
7 E : Positive;
8 Result : out Integer;
9 Found : out Boolean);
(continues on next page)
11 end Show_Search_Array;
3 procedure Search_Array
4 (A : Array_Of_Positives;
5 E : Positive;
6 Result : out Integer;
7 Found : out Boolean) is
8 begin
9 for I in A'Range loop
10 if A (I) = E then
11 Result := I;
12 Found := True;
13 return;
14 end if;
15 end loop;
16 Found := False;
17 end Search_Array;
18
19 end Show_Search_Array;
Prover output
GNATprove produces a message saying that Result is possibly uninitialized on return. There are
perfectly legal uses of the function Search_Array, but flow analysis detects that Result is not
initialized on the path that falls through from the loop. Even though this program is correct, you
shouldn't ignore the message: it means flow analysis cannot guarantee that Result is always ini-
tialized at the call site and so assumes any read of Result at the call site will read initialized data.
Therefore, you should either initialize Result when Found is false, which silences flow analysis, or
verify this assumption at each call site by other means.
2.5.2 Example #2
To avoid the message previously issued by GNATprove, we modify Search_Array to raise an ex-
ception when E isn't found in A:
5 Not_Found : exception;
6
7 procedure Search_Array
8 (A : Array_Of_Positives;
9 E : Positive;
10 Result : out Integer);
11 end Show_Search_Array;
3 procedure Search_Array
4 (A : Array_Of_Positives;
5 E : Positive;
6 Result : out Integer) is
7 begin
8 for I in A'Range loop
9 if A (I) = E then
10 Result := I;
11 return;
12 end if;
13 end loop;
14 raise Not_Found;
15 end Search_Array;
16
17 end Show_Search_Array;
Prover output
Flow analysis doesn't emit any messages in this case, meaning it can verify that Result can't be
read in SPARK code while uninitialized. But why is that, since Result is still not initialized when E is
not in A? This is because the exception, Not_Found, can never be caught within SPARK code (SPAK
doesn't allow exception handlers). However, the GNATprove tool also tries to ensure the absence
of runtime errors in SPARK code, so tries to prove that Not_Found is never raised. When it can't
do that here, it produces a different message.
2.5.3 Example #3
In this example, we're using a discriminated record for the result of Search_Array instead of
conditionally raising an exception. By using such a structure, the place to store the index at which
E was found exists only when E was indeed found. So if it wasn't found, there's nothing to be
initialized.
13 procedure Search_Array
14 (A : Array_Of_Positives;
15 E : Positive;
16 Result : out Search_Result)
(continues on next page)
19 end Show_Search_Array;
3 procedure Search_Array
4 (A : Array_Of_Positives;
5 E : Positive;
6 Result : out Search_Result) is
7 begin
8 for I in A'Range loop
9 if A (I) = E then
10 Result := (Found => True,
11 Content => I);
12 return;
13 end if;
14 end loop;
15 Result := (Found => False);
16 end Search_Array;
17
18 end Show_Search_Array;
Prover output
This example is correct and flow analysis doesn't issue any message: it can verify both that no
uninitialized variables are read in Search_Array's body, and that all its outputs are set on return.
We've used the attribute Constrained in the precondition of Search_Array to indicate that the
value of the Result in argument can be set to any variant of the record type Search_Result,
specifically to either the variant where E was found and where it wasn't.
2.5.4 Example #4
8 end Show_Biggest_Increasing_Sequence;
24 Biggest_Seq : Natural := 0;
25
26 begin
27 for I in A'Range loop
28 Test_Index (I);
29 if End_Of_Seq then
30 Biggest_Seq := Natural'Max (Size_Of_Seq, Biggest_Seq);
31 end if;
32 end loop;
33 return Biggest_Seq;
34 end Size_Of_Biggest_Increasing_Sequence;
35
36 end Show_Biggest_Increasing_Sequence;
Prover output
However, this example is not correct. Flow analysis emits messages for Test_Index stating that
Max, Beginning, and Size_Of_Seq should be initialized before being read. Indeed, when you look
carefully, you see that both Max and Beginning are missing initializations because they are read in
Test_Index before being written. As for Size_Of_Seq, we only read its value when End_Of_Seq
is true, so it actually can't be read before being written, but flow analysis isn't able to verify its
initialization by using just flow information.
The call to Test_Index is automatically inlined by GNATprove, which leads to another messages
above. If GNATprove couldn't inline the call to Test_Index, for example if it was defined in another
unit, the same messages would be issued on the call to Test_Index.
2.5.5 Example #5
In the following example, we model permutations as arrays where the element at index I is the
position of the I'th element in the permutation. The procedure Init initializes a permutation to
the identity, where the I'th elements is at the I'th position. Cyclic_Permutation calls Init and
then swaps elements to construct a cyclic permutation.
12 end Show_Permutation;
30 end Show_Permutation;
Prover output
This program is correct. However, flow analysis will nevertheless still emit messages because it
can't verify that every element of A is initialized by the loop in Init. This message is a false alarm.
You can either ignore it or justify it safely.
2.5.6 Example #6
This program is the same as the previous one except that we've changed the mode of A in the
specification of Init to in out to avoid the message from flow analysis on array assignment.
12 end Show_Permutation;
30 end Show_Permutation;
Prover output
This program is not correct. Changing the mode of a parameter that should really be out to in
out to silence a false alarm is not a good idea. Not only does this obfuscate the specification of
Init, but flow analysis emits a message on the procedure where A is not initialized, as shown by
the message in Cyclic_Permutation.
2.5.7 Example #7
9 end Show_Increments;
20 begin
21 for I in A'Range loop
22 if I > A'First then
23 Threshold := A (I - 1);
24 end if;
25 Incr_Until_Threshold (I);
26 end loop;
(continues on next page)
29 end Show_Increments;
Prover output
Everything is fine here. Specifically, the Global contract is correct. It mentions both Threshold,
which is read but not written in the procedure, and A, which is both read and written. The fact that
A is a parameter of an enclosing unit doesn't prevent us from using it inside the Global contract;
it really is global to Incr_Until_Threshold. We didn't mention Increment since it's a static
constant.
2.5.8 Example #8
We now go back to the procedure Test_Index from Example #4 (page 35) and correct the missing
initializations. We want to know if the Global contract of Test_Index is correct.
8 end Show_Biggest_Increasing_Sequence;
28 Biggest_Seq : Natural := 0;
29
30 begin
31 for I in A'Range loop
32 Test_Index (I);
33 if End_Of_Seq then
34 Biggest_Seq := Natural'Max (Size_Of_Seq, Biggest_Seq);
35 end if;
36 end loop;
37 return Biggest_Seq;
38 end Size_Of_Biggest_Increasing_Sequence;
39
40 end Show_Biggest_Increasing_Sequence;
Prover output
2.5.9 Example #9
Next, we change the Global contract of Test_Index into a Depends contract. In general, we
don't need both contracts because the set of global variables accessed can be deduced from the
Depends contract.
8 end Show_Biggest_Increasing_Sequence;
28 Biggest_Seq : Natural := 0;
29
30 begin
31 for I in A'Range loop
32 Test_Index (I);
33 if End_Of_Seq then
34 Biggest_Seq := Natural'Max (Size_Of_Seq, Biggest_Seq);
35 end if;
36 end loop;
37 return Biggest_Seq;
38 end Size_Of_Biggest_Increasing_Sequence;
39
40 end Show_Biggest_Increasing_Sequence;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: analysis of data and information flow ...
show_biggest_increasing_sequence.adb:7:07: info: initialization of "End_Of_Seq"␣
↪proved
This example is correct. Some of the dependencies, such as Size_Of_Seq depending on Begin-
ning, come directly from the assignments in the subprogram. Since the control flow influences the
final value of all of the outputs, the variables that are being read, A, Current_Index, and Max, are
present in every dependency relation. Finally, the dependencies of Size_Of_Eq and Beginning
on themselves are because they may not be modified by the subprogram execution.
The subprogram Identity swaps the value of its parameter two times. Its Depends contract says
that the final value of X only depends on its initial value and likewise for Y.
9 end Show_Swap;
16 end Show_Swap;
Prover output
This code is correct, but flow analysis can't verify the Depends contract of Identity because we
didn't supply a Depends contract for Swap. Therefore, flow analysis assumes that all outputs of
Swap, X and Y, depend on all its inputs, both X and Y's initial values. To prevent this, we should
manually specify a Depends contract for Swap.
THREE
This section presents the proof capability of GNATprove, a major tool for the SPARK language.
We focus here on the simpler proofs that you'll need to write to verify your program's integrity.
The primary objective of performing proof of your program's integrity is to ensure the absence of
runtime errors during its execution.
The analysis steps discussed here are only sound if you've previously performed Flow Analysis
(page 21). You shouldn't proceed further if you still have unjustified flow analysis messages for
your program.
There's always the potential for errors that aren't detected during compilation to occur during a
program's execution. These errors, called runtime errors, are those targeted by GNATprove.
There are various kinds of runtime errors, the most common being references that are out of the
range of an array (buffer overflow12 in Ada), subtype range violations, overflows in computations,
and divisions by zero. The code below illustrates many examples of possible runtime errors, all
within a single statement. Look at the assignment statement setting the I + J'th cell of an array
A to the value P /Q.
Listing 1: show_runtime_errors.ads
1 package Show_Runtime_Errors is
2
7 end Show_Runtime_Errors;
Listing 2: show_runtime_errors.adb
1 package body Show_Runtime_Errors is
2
8 end Show_Runtime_Errors;
Prover output
12 https://en.wikipedia.org/wiki/Buffer_overflow
45
Introduction to SPARK
There are quite a number of errors that may occur when executing this code. If we don't know
anything about the values of I, J, P, and Q, we can't rule out any of those errors.
First, the computation of I + J can overflow, for example if I is Integer'Last and J is positive.
A (Integer'Last + 1) := P / Q;
Next, the sum, which is used as an array index, may not be in the range of the index of the array.
A (A'Last + 1) := P / Q;
On the other side of the assignment, the division may also overflow, though only in the very special
case where P is Integer'First and Q is -1 because of the asymmetric range of signed integer
types.
A (I + J) := Integer'First / -1;
A (I + J) := P / 0;
Finally, since the array contains natural numbers, it's also an error to store a negative value in it.
A (I + J) := 1 / -1;
The compiler generates checks in the executable code corresponding to each of those runtime
errors. Each check raises an exception if it fails. For the above assignment statement, we can see
examples of exceptions raised due to failed checks for each of the different cases above.
A (Integer'Last + 1) := P / Q;
-- raised CONSTRAINT_ERROR : overflow check failed
A (A'Last + 1) := P / Q;
-- raised CONSTRAINT_ERROR : index check failed
A (I + J) := Integer'First / (-1);
-- raised CONSTRAINT_ERROR : overflow check failed
A (I + J) := 1 / (-1);
-- raised CONSTRAINT_ERROR : range check failed
A (I + J) := P / 0;
-- raised CONSTRAINT_ERROR : divide by zero
These runtime checks are costly, both in terms of program size and execution time. It may be
appropriate to remove them if we can statically ensure they aren't needed at runtime, in other
words if we can prove that the condition tested for can never occur.
This is where the analysis done by GNATprove comes in. It can be used to demonstrate statically
that none of these errors can ever occur at runtime. Specifically, GNATprove logically interprets
the meaning of every instruction in the program. Using this interpretation, GNATprove generates
a logical formula called a verification condition for each check that would otherwise be required by
the Ada (and hence SPARK) language.
A (Integer'Last + 1) := P / Q;
-- medium: overflow check might fail
A (A'Last + 1) := P / Q;
-- medium: array index check might fail
A (I + J) := Integer'First / (-1);
-- medium: overflow check might fail
A (I + J) := 1 / (-1);
-- medium: range check might fail
A (I + J) := P / 0;
-- medium: divide by zero might fail
GNATprove then passes these verification conditions to an automatic prover, stated as conditions
that must be true to avoid the error. If every such condition can be validated by a prover (meaning
that it can be mathematically shown to always be true), we've been able to prove that no error can
ever be raised at runtime when executing that program.
3.2 Modularity
Listing 3: show_modularity.adb
1 procedure Show_Modularity is
2
3.2. Modularity 47
Introduction to SPARK
10 X : Integer;
11 begin
12 X := Integer'Last - 2;
13 Increment (X);
14 -- After the call, GNATprove no longer knows the value of X
15
16 X := X + 1;
17 -- medium: overflow check might fail
18 end Show_Modularity;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
show_modularity.adb:6:14: info: overflow check proved
show_modularity.adb:10:04: info: initialization of "X" proved
show_modularity.adb:13:04: info: precondition proved
show_modularity.adb:16:04: warning: possibly useless assignment to "X", value␣
↪might not be referenced [-gnatwm]
3.2.1 Exceptions
There are two cases where GNATprove doesn't require modularity and hence doesn't make the
above assumptions. First, local subprograms without contracts can be inlined if they're simple
enough and are neither recursive nor have multiple return points. If we remove the contract from
Increment, it fits the criteria for inlining.
Listing 4: show_modularity.adb
1 procedure Show_Modularity is
2
9 X : Integer;
10 begin
11 X := Integer'Last - 2;
12 Increment (X);
13 X := X + 1;
14 -- info: overflow check proved
15 end Show_Modularity;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
show_modularity.adb:5:14: info: overflow check proved, in call inlined at show_
↪modularity.adb:12
GNATprove now sees the call to Increment exactly as if the increment on X was done outside that
call, so it can successfully verify that neither addition can overflow.
Note: For more details on contextual analysis of subprograms, see the SPARK User's Guide13 .
The other case involves functions. If we define a function as an expression function, with or without
contracts, GNATprove uses the expression itself as the postcondition on the result of the function.
In our example, replacing Increment with an expression function allows GNATprove to success-
fully verify the overflow check in the addition.
Listing 5: show_modularity.adb
1 procedure Show_Modularity is
2
8 X : Integer;
9 begin
10 X := Integer'Last - 2;
11 X := Increment (X);
12 X := X + 1;
13 -- info: overflow check proved
14 end Show_Modularity;
Prover output
Note: For more details on expression functions, see the SPARK User's Guide14 .
13 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/how_to_write_subprogram_contracts.
html#contextual-analysis-of-subprograms-without-contracts
14 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/specification_features.html#
expression-functions
3.2. Modularity 49
Introduction to SPARK
3.3 Contracts
Ada contracts are perfectly suited for formal verification, but are primarily designed to be checked
at runtime. When you specify the -gnata switch, the compiler generates code that verifies the
contracts at runtime. If an Ada contract isn't satisfied for a given subprogram call, the program
raises the Assert_Failure exception. This switch is particularly useful during development and
testing, but you may also retain run-time execution of assertions, and specifically preconditions,
during the program's deployment to avoid an inconsistent state.
Consider the incorrect call to Increment below, which violates its precondition. One way to de-
tect this error is by compiling the function with assertions enabled and testing it with inputs that
trigger the violation. Another way, one that doesn't require guessing the needed inputs, is to run
GNATprove.
Listing 6: show_precondition_violation.adb
1 procedure Show_Precondition_Violation is
2
9 X : Integer;
10
11 begin
12 X := Integer'Last;
13 Increment (X);
14 end Show_Precondition_Violation;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
show_precondition_violation.adb:13:04: medium: precondition might fail, cannot␣
↪prove X < Integer'last
Runtime output
Similarly, consider the incorrect implementation of function Absolute below, which violates its
postcondition. Likewise, one way to detect this error is by compiling the function with assertions
enabled and testing with inputs that trigger the violation. Another way, one which again doesn't
require finding the inputs needed to demonstrate the error, is to run GNATprove.
Listing 7: show_postcondition_violation.adb
1 procedure Show_Postcondition_Violation is
2
11 X : Integer;
12
13 begin
14 X := 1;
15 Absolute (X);
16 end Show_Postcondition_Violation;
Prover output
Runtime output
The benefits of dynamically checking contracts extends beyond making testing easier. Early failure
detection also allows an easier recovery and facilitates debugging, so you may want to enable these
checks at runtime to terminate execution before some damaging or hard-to-debug action occurs.
GNATprove statically analyses preconditions and postconditions. It verifies preconditions every
time a subprogram is called, which is the runtime semantics of contracts. Postconditions, on the
other hand, are verified once as part of the verification of the subprogram's body. For example,
GNATprove must wait until Increment is improperly called to detect the precondition violation,
since a precondition is really a contract for the caller. On the other hand, it doesn't need Absolute
to be called to detect that its postcondition doesn't hold for all its possible inputs.
Note: For more details on pre and postconditions, see the SPARK User's Guide15 .
Expressions in Ada contracts have the same semantics as Boolean expressions elsewhere, so run-
time errors can occur during their computation. To simplify both debugging of assertions and
combining testing and static verification, the same semantics are used by GNATprove.
While proving programs, GNATprove verifies that no error can ever be raised during the execution
of the contracts. However, you may sometimes find those semantics too heavy, in particular with
respect to overflow checks, because they can make it harder to specify an appropriate precondition.
We see this in the function Add below.
Listing 8: show_executable_semantics.adb
1 procedure Show_Executable_Semantics
2 with SPARK_Mode => On
3 is
4 function Add (X, Y : Integer) return Integer is (X + Y)
5 with Pre => X + Y in Integer;
6
7 X : Integer;
(continues on next page)
15 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/subprogram_contracts.html#
preconditions
3.3. Contracts 51
Introduction to SPARK
Build output
Prover output
Runtime output
GNATprove issues a message on this code warning about a possible overflow when computing
the sum of X and Y in the precondition. Indeed, since expressions in assertions have normal Ada
semantics, this addition can overflow, as you can easily see by compiling and running the code that
calls Add with arguments Integer'Last and 1.
On the other hand, you sometimes may prefer GNATprove to use the mathematical semantics of
addition in contracts while the generated code still properly verifies that no error is ever raised
at runtime in the body of the program. You can get this behavior by using the compiler switch
-gnato?? (for example -gnato13), which allows you to independently set the overflow mode in
code (the first digit) and assertions (the second digit). For both, you can either reduce the number
of overflow checks (the value 2), completely eliminate them (the value 3), or preserve the default
Ada semantics (the value 1).
Note: For more details on overflow modes, see the SPARK User's Guide16 .
16 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/overflow_modes.html
As we've seen, a key feature of SPARK is that it allows us to state properties to check using assertions
and contracts. SPARK supports preconditions and postconditions as well as assertions introduced
by the Assert pragma.
The SPARK language also includes new contract types used to assist formal verification. The new
pragma Assume is treated as an assertion during execution but introduces an assumption when
proving programs. Its value is a Boolean expression which GNATprove assumes to be true without
any attempt to verify that it's true. You'll find this feature useful, but you must use it with great
care. Here's an example of using it.
Listing 9: incr.adb
1 procedure Incr (X : in out Integer) is
2 begin
3 pragma Assume (X < Integer'Last);
4 X := X + 1;
5 end Incr;
Prover output
Note: For more details on pragma Assume, see the SPARK User's Guide17 .
The Contract_Cases aspect is another construct introduced for GNATprove, but which also acts
as an assertion during execution. It allows you to specify the behavior of a subprogram using a
disjunction of cases. Each element of a Contract_Cases aspect is a guard, which is evaluated
before the call and may only reference the subprogram's inputs, and a consequence. At each call
of the subprogram, one and only one guard is permitted to evaluate to True. The consequence of
that case is a contract that's required to be satisfied when the subprogram returns.
Prover output
17 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/assertion_pragmas.html#
pragma-assume
3.3. Contracts 53
Introduction to SPARK
Note: For more details on Contract_Cases, see the SPARK User's Guide18 .
GNATprove may report an error while verifying a program for any of the following reasons:
• there might be an error in the program; or
• the property may not be provable as written because more information is required; or
• the prover used by GNATprove may be unable to prove a perfectly valid property.
We spend the remainder of this section discussing the sometimes tricky task of debugging failed
proof attempts.
First, let's discuss the case where there's indeed an error in the program. There are two possibili-
ties: the code may be incorrect or, equally likely, the specification may be incorrect. As an example,
there's an error in our procedure Incr_Until below which makes its Contract_Cases unprov-
able.
10 end Show_Failed_Proof_Attempt;
13 end Show_Failed_Proof_Attempt;
18 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/subprogram_contracts.html#
contract-cases
Prover output
Since this is an assertion that can be executed, it may help you find the problem if you run the
program with assertions enabled on representative sets of inputs. This allows you to find bugs in
both the code and its contracts. In this case, testing Incr_Until with an input greater than 1000
raises an exception at runtime.
10 end Show_Failed_Proof_Attempt;
13 end Show_Failed_Proof_Attempt;
3 procedure Main is
4 Dummy : Integer;
5 begin
6 Dummy := 0;
7 Incr_Until (Dummy);
8
9 Dummy := 1000;
10 Incr_Until (Dummy);
11 end Main;
Prover output
Runtime output
The error message shows that the first contract case is failing, which means that Incremented is
True. However, if we print the value of Incremented before returning, we see that it's False, as
expected for the input we provided. The error here is that guards of contract cases are evaluated
before the call, so our specification is wrong! To correct this, we should either write X < 1000 as
the guard of the first case or use a standard postcondition with an if-expression.
Even if both the code and the assertions are correct, GNATprove may still report that it can't prove
a verification condition for a property. This can happen for two reasons:
• The property may be unprovable because the code is missing some assertion. One category
of these cases is due to the modularity of the analysis which, as we discussed above, means
that GNATprove only knows about the properties of your subprograms that you have explicitly
written.
• There may be some information missing in the logical model of the program used by GNAT-
prove.
Let's look at the case where the code and the specification are correct but there's some information
missing. As an example, GNATprove finds the postcondition of Increase to be unprovable.
3 C : Natural := 100;
4
8 end Show_Failed_Proof_Attempt;
14 end Show_Failed_Proof_Attempt;
Prover output
This postcondition is a conditional. It says that if the parameter (X) is less than a certain value (C),
its value will be increased by the procedure while if it's greater, its value will be set to C (saturated).
When C has the value 100, the code of Increases adds 10 to the value of X if it was initially less
than 90, increments X by 1 if it was between 90 and 99, and sets X to 100 if it was greater or equal
to 100. This behavior does satisfy the postcondition, so why is the postcondition not provable?
The values in the counterexample returned by GNATprove in its message gives us a clue: C = 0
and X = 10 and X'Old = 0. Indeed, if C is not equal to 100, our reasoning above is incorrect: the
values of 0 for C and X on entry indeed result in X being 10 on exit, which violates the postcondition!
We probably didn't expect the value of C to change, or at least not to go below 90. But, in that case,
we should have stated so by either declaring C to be constant or by adding a precondition to the
Increase subprogram. If we do either of those, GNATprove is able to prove the postcondition.
Finally, there are cases where GNATprove provides a perfectly valid verification condition for a
property, but it's nevertheless not proved by the automatic prover that runs in the later stages of
the tool's execution. This is quite common. Indeed, GNATprove produces its verification conditions
in first-order logic, which is not decidable, especially in combination with the rules of arithmetic.
Sometimes, the automatic prover just needs more time. Other times, the prover will abandon the
search almost immediately or loop forever without reaching a conclusive answer (either a proof or
a counterexample).
For example, the postcondition of our GCD function below — which calculates the value of the GCD
of two positive numbers using Euclide's algorithm — can't be verified with GNATprove's default
settings.
8 end Show_Failed_Proof_Attempt;
14 end Show_Failed_Proof_Attempt;
Prover output
The first thing we try is increasing the amount of time the prover is allowed to spend on each
verification condition using the --timeout option of GNATprove (e.g., by using the dialog box in
GNAT Studio). In this example, increasing it to one minute, which is relatively high, doesn't help. We
can also specify an alternative automatic prover — if we have one — using the option --prover
of GNATprove (or the dialog box). For our postcondition, we tried Alt-Ergo, CVC4, and Z3 without
any luck.
8 end Show_Failed_Proof_Attempt;
25 end Show_Failed_Proof_Attempt;
Prover output
To better understand the reason for the failure, we added intermediate assertions to simplify the
proof and pin down the part that's causing the problem. Adding such assertions is often a good
idea when trying to understand why a property is not proved. Here, provers can't verify that if both
A - B and B can be divided by Result so can A. This may seem surprising, but non-linear arithmetic,
involving, for example, multiplication, modulo, or exponentiation, is a difficult topic for provers and
is not handled very well in practice by any of the general-purpose ones like Alt-Ergo, CVC4, or Z3.
Note: For more details on how to investigate unproved checks, see the SPARK User's Guide19 .
3.5.1 Example #1
The package Lists defines a linked-list data structure. We call Link(I,J) to make a link from
index I to index J and call Goes_To(I,J) to determine if we've created a link from index I to
index J. The postcondition of Link uses Goes_To to state that there must be a link between its
arguments once Link completes.
7 procedure Link (I, J : Index) with Post => Goes_To (I, J);
8
9 private
10
html
22 Memory : Cell_Array;
23
24 end Lists;
16 end Lists;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
lists.ads:7:47: medium: postcondition might fail [possible fix: you should␣
↪consider adding a postcondition to function Goes_To or turning it into an␣
↪expression function]
This example is correct, but can't be verified by GNATprove. This is because Goes_To itself has no
postcondition, so nothing is known about its result.
3.5.2 Example #2
7 procedure Link (I, J : Index) with Post => Goes_To (I, J);
(continues on next page)
9 private
10
22 Memory : Cell_Array;
23
27 end Lists;
8 end Lists;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
lists.adb:5:18: info: discriminant check proved
lists.ads:7:47: info: postcondition proved
lists.ads:25:44: info: discriminant check proved
GNATprove can fully prove this version: Goes_To is an expression function, so its body is available
for proof (specifically, for creating the postcondition needed for the proof).
3.5.3 Example #3
The package Stacks defines an abstract stack type with a Push procedure that adds an element
at the top of the stack and a function Peek that returns the content of the element at the top of
the stack (without removing it).
23 end Stacks;
9 S.Top := S.Top + 1;
10 S.Content (S.Top) := E;
11 end Push;
12
13 end Stacks;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
stacks.ads:7:14: medium: postcondition might fail
gnatprove: unproved check messages considered as errors
This example isn't correct. The postcondition of Push is only satisfied if the stack isn't full when we
call Push.
3.5.4 Example #4
We now change the behavior of Push so it raises an exception when the stack is full instead of
returning.
5 Is_Full_E : exception;
6
25 end Stacks;
9 S.Top := S.Top + 1;
10 S.Content (S.Top) := E;
11 end Push;
12
13 end Stacks;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
stacks.adb:6:10: medium: exception might be raised
gnatprove: unproved check messages considered as errors
The postcondition of Push is now proved because GNATprove only considers execution paths lead-
ing to normal termination. But it issues a message warning that exception Is_Full_E may be
raised at runtime.
3.5.5 Example #5
Let's add a precondition to Push stating that the stack shouldn't be full.
5 Is_Full_E : exception;
6
13 private
14
28 end Stacks;
12 end Stacks;
Prover output
This example is correct. With the addition of the precondition, GNATprove can now verify that
Is_Full_E can never be raised at runtime.
3.5.6 Example #6
The package Memories defines a type Chunk that models chunks of memory. Each element of the
array, represented by its index, corresponds to one data element. The procedure Read_Record
reads two pieces of data starting at index From out of the chunk represented by the value of Mem-
ory.
10 end Memories;
20 begin
21 Data1 := Read_One (From, 1);
22 Data2 := Read_One (From, 2);
23 end Read_Record;
Prover output
This example is correct, but it can't be verified by GNATprove, which analyses Read_One on its own
and notices that an overflow may occur in its precondition in certain contexts.
3.5.7 Example #7
10 end Memories;
21 begin
22 Data1 := Read_One (From, 1);
23 Data2 := Read_One (From, 2);
24 end Read_Record;
Prover output
This example is also not correct: unfortunately, our attempt to correct Read_One's precondition
failed. For example, an overflow will occur at runtime if First is Integer'Last and Memory'Last
is negative. This is possible here because type Chunk uses Integer as base index type instead of
Natural or Positive.
3.5.8 Example #8
10 end Memories;
18 begin
19 Data1 := Read_One (From, 1);
20 Data2 := Read_One (From, 2);
21 end Read_Record;
Prover output
This example is correct and fully proved. We could have fixed the contract of Read_One to cor-
rectly handle both positive and negative values of Memory'Last, but we found it simpler to let the
function be inlined for proof by removing its precondition.
3.5.9 Example #9
The procedure Compute performs various computations on its argument. The computation per-
formed depends on its input range and is reflected in its contract, which we express using a Con-
tract_Cases aspect.
Prover output
This example isn't correct. We duplicated the content of Compute's body in its contract. This is
incorrect because the semantics of Contract_Cases require disjoint cases, just like a case state-
ment. The counterexample returned by GNATprove shows that X = 0 is covered by two different
case-guards (the first and the second).
Prover output
This example is still not correct. GNATprove can successfully prove the different cases are disjoint
and also successfully verify each case individually. This isn't enough, though: a Contract_Cases
must cover all cases. Here, we forgot the value -200, which is what GNATprove reports in its coun-
terexample.
FOUR
STATE ABSTRACTION
Abstraction is a key concept in programming that can drastically simplify both the implementation
and maintenance of code. It's particularly well suited to SPARK and its modular analysis. This
section explains what state abstraction is and how you use it in SPARK. We explain how it impacts
GNATprove's analysis both in terms of information flow and proof of program properties.
State abstraction allows us to:
• express dependencies that wouldn't otherwise be expressible because some data that's read
or written isn't visible at the point where a subprogram is declared — examples are depen-
dencies on data, for which we use the Global contract, and on flow, for which we use the
Depends contract.
• reduce the number of variables that need to be considered in flow analysis and proof, a re-
duction which may be critical in order to scale the analysis to programs with thousands of
global variables.
Abstraction is an important part of programming language design. It provides two views of the
same object: an abstract one and a refined one. The abstract one — usually called specification
— describes what the object does in a coarse way. A subprogram's specification usually describes
how it should be called (e.g., parameter information such as how many and of what types) as well
as what it does (e.g., returns a result or modifies one or more of its parameters).
Contract-based programming, as supported in Ada, allows contracts to be added to a subprogram's
specification. You use contracts to describe the subprogram's behavior in a more fine-grained
manner, but all the details of how the subprogram actually works are left to its refined view, its
implementation.
Take a look at the example code shown below.
Listing 1: increase.ads
1 procedure Increase (X : in out Integer) with
2 Global => null,
3 Pre => X <= 100,
4 Post => X'Old < X;
Listing 2: increase.adb
1 procedure Increase (X : in out Integer) is
2 begin
3 X := X + 1;
4 end Increase;
Prover output
71
Introduction to SPARK
We've written a specification of the subprogram Increase to say that it's called with a single ar-
gument, a variable of type Integer whose initial value is less than 100. Our contract says that the
only effect of the subprogram is to increase the value of its argument.
Listing 3: increase.ads
1 procedure Increase (X : in out Integer) with
2 Global => null,
3 Pre => X <= 100,
4 Post => X'Old < X;
Listing 4: client.adb
1 with Increase;
2 procedure Client is
3 X : Integer := 0;
4 begin
5 while X <= 100 loop -- The loop will terminate
6 Increase (X); -- Increase can be called safely
7 end loop;
8 pragma Assert (X = 101); -- Will this hold?
9 end Client;
Prover output
Callers can also assume that the implementation of Increase won't cause any runtime errors
when called in the loop. On the other hand, nothing in the specification guarantees that the asser-
tion show above is correct: it may fail if Increase's implementation is changed.
If you follow this basic principle, abstraction can bring you significant benefits. It simplifies both
your program's implementation and verification. It also makes maintenance and code reuse much
easier since changes to the implementation of an object shouldn't affect the code using this object.
Your goal in using it is that it should be enough to understand the specification of an object in order
to use that object, since understanding the specification is usually much simpler than understand-
ing the implementation.
GNATprove relies on the abstraction defined by subprogram contracts and therefore doesn't prove
the assertion after the loop in Client above.
Subprograms aren't the only objects that benefit from abstraction. The state of a package — the
set of persistent variables defined in it — can also be hidden from external users. You achieve
this form of abstraction — called state abstraction — by defining variables in the body or private
part of a package so they can only be accessed through subprogram calls. For example, our Stack
package shown below provides an abstraction for a Stack object which can only be modified using
the Pop and Push procedures.
package Stack is
procedure Pop (E : out Element);
procedure Push (E : in Element);
end Stack;
The fact that we implemented it using an array is irrelevant to the caller. We could change that
without impacting our callers' code.
Hidden state influences a program's behavior, so SPARK allows that state to be declared. You can
use the Abstract_State aspect, an abstraction that names a state, to do this, but you aren't
required to use it even for a package with hidden state. You can use several state abstractions to
declare the hidden state of a single package or you can use it for a package with no hidden state
at all. However, since SPARK doesn't allow aliasing, different state abstractions must always refer
to disjoint sets of variables. A state abstraction isn't a variable: it doesn't have a type and can't be
used inside expressions, either those in bodies or contracts.
As an example of the use of this aspect, we can optionally define a state abstraction for the entire
hidden state of the Stack package like this:
Remember: a state abstraction isn't a variable (it has no type) and can't be used inside expressions.
For example:
Once you've declared an abstract state in a package, you must refine it into its constituents using a
Refined_State aspect. You must place the Refined_State aspect on the package body even if
the package wouldn't otherwise have required a body. For each state abstraction you've declared
for the package, you list the set of variables represented by that state abstraction in its refined
state.
If you specify an abstract state for a package, it must be complete, meaning you must have listed ev-
ery hidden variable as part of some state abstraction. For example, we must add a Refined_State
aspect on our Stack package's body linking the state abstraction (The_Stack) to the entire hidden
state of the package, which consists of both Content and Top.
Listing 5: stack.ads
1 package Stack with
2 Abstract_State => The_Stack
3 is
4 type Element is new Integer;
5
9 end Stack;
Listing 6: stack.adb
1 package body Stack with
2 Refined_State => (The_Stack => (Content, Top))
3 is
4 Max : constant := 100;
5
25 end Stack;
Prover output
You can refine state abstractions in the package body, where all the variables are visible. When
only the package's specification is available, you need a way to specify which state abstraction each
private variable belongs to. You do this by adding the Part_Of aspect to the variable's declaration.
Part_Of annotations are mandatory: if you gave a package an abstract state annotation, you must
link all the hidden variables defined in its private part to a state abstraction. For example:
Listing 7: stack.ads
1 package Stack with
2 Abstract_State => The_Stack
3 is
4 type Element is new Integer;
5
9 private
10
18 end Stack;
Prover output
Since we chose to define Content and Top in Stack's private part instead of its body, we had to
add a Part_Of aspect to both of their declarations, associating them with the state abstraction
The_Stack, even though it's the only state abstraction. However, we still need to list them in the
Refined_State aspect in Stack's body.
So far, we've only discussed hidden variables. But variables aren't the only component of a pack-
age's state. If a package P contains a nested package, the nested package's state is also part of P's
state. If the nested package is hidden, its state is part of P's hidden state and must be listed in P's
state refinement.
We see this in the example below, where the package Hidden_Nested's hidden state is part of P's
hidden state.
Listing 8: p.ads
1 package P with
2 Abstract_State => State
3 is
4 package Visible_Nested with
5 Abstract_State => Visible_State
6 is
7 procedure Get (E : out Integer);
8 end Visible_Nested;
9 end P;
Listing 9: p.adb
1 package body P with
2 Refined_State => (State => Hidden_Nested.Hidden_State)
3 is
4 package Hidden_Nested with
5 Abstract_State => Hidden_State,
6 Initializes => Hidden_State
7 is
8 function Get return Integer;
9 end Hidden_Nested;
10
Prover output
Any visible state of Hidden_Nested would also have been part of P's hidden state. However,
if P contains a visible nested package, that nested package's state isn't part of P's hidden state.
Instead, you should declare that package's hidden state in a separate state abstraction on its own
declaration, like we did above for Visible_Nested.
Some constants are also possible components of a state abstraction. These are constants whose
value depends either on a variable or a subprogram parameter. They're handled as variables dur-
ing flow analysis because they participate in the flow of information between variables throughout
the program. Therefore, GNATprove considers these constants to be part of a package's state just
like it does for variables.
If you've specified a state abstraction for a package, you must list such hidden constants declared
in that package in the state abstraction refinement. However, constants that don't depend on
variables don't participate in the flow of information and must not appear in a state refinement.
Let's look at this example.
Prover output
Here, Max — the maximum number of elements that can be stored in the stack — is initialized
from a variable in an external package. Because of this, we must include Max as part of the state
abstraction The_Stack.
Note: For more details on state abstractions, see the SPARK User's Guide20 .
Hidden variables can only be accessed through subprogram calls, so you document how state ab-
stractions are modified during the program's execution via the contracts of those subprograms.
You use Global and Depends contracts to specify which of the state abstractions are used by
a subprogram and how values flow through the different variables. The Global and Depends
contracts that you write when referring to state abstractions are often less precise than contracts
referring to visible variables since the possibly different dependencies of the hidden variables con-
tained within a state abstraction are collapsed into a single dependency.
Let's add Global and Depends contracts to the Pop procedure in our stack.
12 end Stack;
Prover output
In this example, the Pop procedure only modifies the value of the hidden variable Top, while Con-
tent is unchanged. By using distinct state abstractions for the two variables, we're able to preserve
this semantic in the contract.
20 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/package_contracts.html#
state-abstraction
Let's contrast this example with a different representation of Global and Depends contracts, this
time using a single abstract state.
10 end Stack;
Prover output
Here, Top_State and Content_State are merged into a single state abstraction, The_Stack.
By doing so, we've hidden the fact that Content isn't modified (though we're still showing that
Top may be modified). This loss in precision is reasonable here, since it's the whole point of the
abstraction. However, you must be careful not to aggregate unrelated hidden state because this
risks their annotations becoming meaningless.
Even though imprecise contracts that consider state abstractions as a whole are perfectly reason-
able for users of a package, you should write Global and Depends contracts that are as precise as
possible within the package body. To allow this, SPARK introduces the notion of refined contracts,
which are precise contracts specified on the bodies of subprograms where state refinements are
visible. These contracts are the same as normal Global and Depends contracts except they refer
directly to the hidden state of the package.
When a subprogram is called inside the package body, you should write refined contracts instead
of the general ones so that the verification can be as precise as possible. However, refined Global
and Depends are optional: if you don't specify them, GNATprove will compute them to check the
package's implementation.
For our Stack example, we could add refined contracts as shown below.
14 end Stack;
31 end Stack;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: analysis of data and information flow ...
15 end Stack;
25 end Stack;
Prover output
Just like we saw for Global and Depends contracts, you may often find it useful to have a more
precise view of functional contracts in the context where the hidden variables are visible. You do
this using expression functions in the same way we did for the functions Is_Empty and Is_Full
above. As expression function, bodies act as contracts for GNATprove, so they automatically give
a more precise version of the contracts when their implementation is visible.
You may often need a more constraining contract to verify the package's implementation but want
to be less strict outside the abstraction. You do this using the Refined_Post aspect. This aspect,
when placed on a subprogram's body, provides stronger guarantees to internal callers of a subpro-
gram. If you provide one, the refined postcondition must imply the subprogram's postcondition.
This is checked by GNATprove, which reports a failing postcondition if the refined postcondition is
too weak, even if it's actually implied by the subprogram's body. SPARK doesn't peform a similar
verification for normal preconditions.
For example, we can refine the postconditions in the bodies of Pop and Push to be more detailed
than what we wrote for them in their specification.
15 end Stack;
29 end Stack;
Prover output
Note: For more details on refinement in contracts, see the SPARK User's Guide21 .
As part of flow analysis, GNATprove checks for the proper initialization of variables. Therefore, flow
analysis needs to know which variables are initialized during the package's elaboration.
You can use the Initializes aspect to specify the set of visible variables and state abstractions
that are initialized during the elaboration of a package. An Initializes aspect can't refer to a
variable that isn't defined in the unit since, in SPARK, a package can only initialize variables declared
immediately within the package.
Initializes aspects are optional. If you don't supply any, they'll be derived by GNATprove.
For our Stack example, we could add an Initializes aspect.
9 end Stack;
17 end Stack;
Prover output
21 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/subprogram_contracts.html#
state-abstraction-and-contracts
Flow analysis also checks for dependencies between variables, so it must be aware of how informa-
tion flows through the code that performs the initialization of states. We discussed one use of the
Initializes aspect above. But you also can use it to provide flow information. If the initial value
of a variable or state abstraction is dependent on the value of another visible variable or state ab-
straction from another package, you must list this dependency in the Initializes contract. You
specify the list of entities on which a variable's initial value depends using an arrow following that
variable's name.
Let's look at this example:
Prover output
Here we indicated that V2's initial value depends on the value of Q.External_Variable by in-
cluding that dependency in the Initializes aspect of P. We didn't list any dependency for V1
because its initial value doesn't depend on any external variable. We could also have stated that
lack of dependency explicitly by writing V1 => null.
GNATprove computes dependencies of initial values if you don't supply an Initializes aspect.
However, if you do provide an Initializes aspect for a package, it must be complete: you must
list every initialized state of the package, along with all its external dependencies.
Note: For more details on Initializes, see the SPARK User's Guide22 .
22 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/package_contracts.html#
package-initialization
4.10.1 Example #1
Package Communication defines a hidden local package, Ring_Buffer, whose capacity is initial-
ized from an external configuration during elaboration.
3 External_Variable : Natural := 1;
4
5 end Configuration;
9 private
10
17 end Communication;
10 end Communication;
Prover output
This example isn't correct. Capacity is declared in the private part of Communication. Therefore,
we should have linked it to State by using the Part_Of aspect in its declaration.
4.10.2 Example #2
Let's add Part_Of to the state of hidden local package Ring_Buffer, but this time we hide variable
Capacity inside the private part of Ring_Buffer.
3 External_Variable : Natural := 1;
4
5 end Configuration;
18 end Communication;
11 end Communication;
Prover output
4.10.3 Example #3
Package Counting defines two counters: Black_Counter and Red_Counter. It provides sepa-
rate initialization procedures for each, both called from the main procedure.
6 procedure Reset_Black_Count is
7 begin
8 Black_Counter := 0;
9 end Reset_Black_Count;
10
11 procedure Reset_Red_Count is
12 begin
13 Red_Counter := 0;
14 end Reset_Red_Count;
15 end Counting;
3 procedure Main is
4 begin
5 Reset_Black_Count;
6 Reset_Red_Count;
7 end Main;
Prover output
This program doesn't read any uninitialized data, but GNATprove fails to verify that. This is because
we provided a state abstraction for package Counting, so flow analysis computes the effects of
subprograms in terms of this state abstraction and thus considers State to be an in-out global con-
sisting of both Black_Counter and Red_Counter. So it issues the message requiring that State
be initialized after elaboration as well as the warning that no procedure in package Counting can
initialize its state.
4.10.4 Example #4
4 procedure Reset_Black_Count is
5 begin
6 Black_Counter := 0;
7 end Reset_Black_Count;
8
9 procedure Reset_Red_Count is
10 begin
11 Red_Counter := 0;
12 end Reset_Red_Count;
13 end Counting;
3 procedure Main is
4 begin
5 Reset_Black_Count;
6 Reset_Red_Count;
7 end Main;
Prover output
This example is correct. Because we didn't provide a state abstraction, GNATprove reasons in terms
of variables, instead of states, and proves data initialization without any problem.
4.10.5 Example #5
Let's restore the abstract state to package Counting, but this time provide a procedure Reset_All
that calls the initialization procedures Reset_Black_Counter and Reset_Red_Counter.
6 procedure Reset_Black_Count is
7 begin
8 Black_Counter := 0;
9 end Reset_Black_Count;
10
11 procedure Reset_Red_Count is
12 begin
13 Red_Counter := 0;
14 end Reset_Red_Count;
15
16 procedure Reset_All is
17 begin
18 Reset_Black_Count;
19 Reset_Red_Count;
20 end Reset_All;
21 end Counting;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: analysis of data and information flow ...
counting.ads:4:37: info: data dependencies proved
counting.ads:5:37: info: data dependencies proved
counting.ads:6:14: info: initialization of "Black_Counter" constituent of "State"␣
↪proved
This example is correct. Flow analysis computes refined versions of Global contracts for inter-
nal calls and uses these to verify that Reset_All indeed properly initializes State. The Re-
fined_Global and Global annotations are not mandatory and can be computed by GNATprove.
4.10.6 Example #6
Build output
Prover output
This example isn't correct. There's a compilation error in Push's postcondition: The_Stack is a
state abstraction, not a variable, and therefore can't be used in an expression.
4.10.7 Example #7
In this version of our abstract stack unit, a copy of the stack is returned by function Get_Stack,
which we call in the postcondition of Push to specify that the stack shouldn't be modified if it's full.
We also assert that after we push an element on the stack, either the stack is unchanged (if it was
already full) or its top element is equal to the element just pushed.
22 private
23
29 end Stack;
22 end Stack;
Prover output
This program is correct, but GNATprove can't prove the assertion in Use_Stack. Indeed, even
if Get_Stack is an expression function, its body isn't visible outside of Stack's body, where it's
defined.
4.10.8 Example #8
Let's move the definition of Get_Stack and other expression functions inside the private part of
the spec of Stack.
22 private
23
39 end Stack;
14 end Stack;
Prover output
This example is correct. GNATprove can verify the assertion in Use_Stack because it has visibility
to Get_Stack's body.
4.10.9 Example #9
Package Data defines three variables, Data_1, Data_2 and Data_3, that are initialized at elabora-
tion (in Data's package body) from an external interface that reads the file system.
8 Data_1 : Data_Type_1;
9 Data_2 : Data_Type_2;
10 Data_3 : Data_Type_3;
11
12 end Data;
Prover output
This example isn't correct. The dependency between Data_1's, Data_2's, and Data_3's initial
values and File_System must be listed in Data's Initializes aspect.
3 package Data is
4 pragma Elaborate_Body;
5
6 Data_1 : Data_Type_1;
7 Data_2 : Data_Type_2;
8 Data_3 : Data_Type_3;
9
10 end Data;
Prover output
This example is correct. Since Data has no Initializes aspect, GNATprove computes the set of
variables initialized during its elaboration as well as their dependencies.
FIVE
This section is dedicated to the functional correctness of programs. It presents advanced proof
features that you may need to use for the specification and verification of your program's complex
properties.
When we speak about the correctness of a program or subprogram, we mean the extent to which it
complies with its specification. Functional correctness is specifically concerned with properties that
involve the relations between the subprogram's inputs and outputs, as opposed to other properties
such as running time or memory consumption.
For functional correctness, we usually specify stronger properties than those required to just prove
program integrity. When we're involved in a certification processes, we should derive these prop-
erties from the requirements of the system, but, especially in non-certification contexts, they can
also come from more informal sources, such as the program's documentation, comments in its
code, or test oracles.
For example, if one of our goals is to ensure that no runtime error is raised when using the result
of the function Find below, it may be enough to know that the result is either 0 or in the range of
A. We can express this as a postcondition of Find.
Listing 1: show_find.ads
1 package Show_Find is
2
8 end Show_Find;
Listing 2: show_find.adb
1 package body Show_Find is
2
97
Introduction to SPARK
13 end Show_Find;
Prover output
Listing 3: show_find.ads
1 package Show_Find is
2
11 end Show_Find;
Listing 4: show_find.adb
1 package body Show_Find is
2
13 end Show_Find;
Prover output
This time, GNATprove can't prove this postcondition automatically, but we'll see later that we can
help GNATprove by providing a loop invariant, which is checked by GNATprove and allows it to
automatically prove the postcondition for Find.
Writing at least part of your program's specification in the form of contracts has many advantages.
You can execute those contracts during testing, which improves the maintainability of the code by
detecting discrepancies between the program and its specification in earlier stages of development.
If the contracts are precise enough, you can use them as oracles to decide whether a given test
passed or failed. In that case, they can allow you to verify the outputs of specific subprograms
while running a larger block of code. This may, in certain contexts, replace the need for you to
perform unit testing, instead allowing you to run integration tests with assertions enabled. Finally,
if the code is in SPARK, you can also use GNATprove to formally prove these contracts.
The advantage of a formal proof is that it verifies all possible execution paths, something which
isn't always possible by running test cases. For example, during testing, the postcondition of the
subprogram Find shown below is checked dynamically for the set of inputs for which Find is called
in that test, but just for that set.
Listing 5: show_find.ads
1 package Show_Find is
2
11 end Show_Find;
Listing 6: show_find.adb
1 package body Show_Find is
2
13 end Show_Find;
Listing 7: use_find.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Show_Find; use Show_Find;
3
Prover output
Runtime output
However, if Find is formally verified, that verification checks its postcondition for all possible in-
puts. During development, you can attempt such verification earlier than testing since it's per-
formed modularly on a per-subprogram basis. For example, in the code shown above, you can
formally verify Use_Find even before you write the body for subprogram Find.
Contracts for functional correctness are usually more complex than contracts for program integrity,
so they more often require you to use the new forms of expressions introduced by the Ada 2012
standard. In particular, quantified expressions, which allow you to specify properties that must
hold for all or for at least one element of a range, come in handy when specifying properties of
arrays.
As contracts become more complex, you may find it useful to introduce new abstractions to im-
prove the readability of your contracts. Expression functions are a good means to this end because
you can retain their bodies in your package's specification.
Finally, some properties, especially those better described as invariants over data than as proper-
ties of subprograms, may be cumbersome to express as subprogram contracts. Type predicates,
which must hold for every object of a given type, are usually a better match for this purpose. Here's
an example.
Listing 8: show_sort.ads
1 package Show_Sort is
2
Prover output
We can use the subtype Sorted_Nat_Array as the type of a variable that must remain sorted
throughout the program's execution. Specifying that an array is sorted requires a rather complex
expression involving quantifiers, so we abstract away this property as an expression function to im-
prove readability. Is_Sorted's body remains in the package's specification and allows users of the
package to retain a precise knowledge of its meaning when necessary. (You must use Nat_Array
as the type of the operand of Is_Sorted. If you use Sorted_Nat_Array, you'll get infinite re-
cursion at runtime when assertion checks are enabled since that function is called to check all
operands of type Sorted_Nat_Array.)
As the properties you need to specify grow more complex, you may have entities that are only
needed because they are used in specifications (contracts). You may find it important to ensure
that these entities can't affect the behavior of the program or that they're completely removed
from production code. This concept, having entities that are only used for specifications, is usually
called having ghost code and is supported in SPARK by the Ghost aspect.
You can use Ghost aspects to annotate any entity including variables, types, subprograms, and
packages. If you mark an entity as Ghost, GNATprove ensures it can't affect the program's behav-
ior. When the program is compiled with assertions enabled, ghost code is executed like normal
code so it can execute the contracts using it. You can also instruct the compiler to not generate
code for ghost entities.
Consider the procedure Do_Something below, which calls a complex function on its input, X, and
wants to check that the initial and modified values of X are related in that complex way.
Listing 9: show_ghost.ads
1 package Show_Ghost is
2
3 type T is record
4 A, B, C, D, E : Boolean;
5 end record;
6
15 end Show_Ghost;
16 end Show_Ghost;
Prover output
Do_Something stores the initial value of X in a ghost constant, X_Init. We reference it in an asser-
tion to check that the computation performed by the call to Do_Some_Complex_Stuff modified
the value of X in the expected manner.
However, X_Init can't be used in normal code, for example to restore the initial value of X.
3 type T is record
4 A, B, C, D, E : Boolean;
5 end record;
6
15 end Show_Ghost;
14 X := X_Init; -- ERROR
15
16 end Do_Something;
17
18 end Show_Ghost;
3 procedure Use_Ghost is
4 X : T := (True, True, False, False, True);
(continues on next page)
Build output
Prover output
When compiling this example, the compiler flags the use of X_Init as illegal, but more complex
cases of interference between ghost and normal code may sometimes only be detected when you
run GNATprove.
Functions used only in specifications are a common occurrence when writing contracts for func-
tional correctness. For example, expression functions used to simplify or factor out common pat-
terns in contracts can usually be marked as ghost.
But ghost functions can do more than improve readability. In real-world programs, it's often the
case that some information necessary for functional specification isn't accessible in the package's
specification because of abstraction.
Making this information available to users of the packages is generally out of the question because
that breaks the abstraction. Ghost functions come in handy in that case since they provide a way
to give access to that information without making it available to normal client code.
Let's look at the following example.
18 private
19
27 end Stacks;
Prover output
Here, the type Stack is private. To specify the expected behavior of the Push procedure, we need
to go inside this abstraction and access the values of the elements stored in S. For this, we introduce
a function Get_Model that returns an array as a representation of the stack. However, we don't
want code that uses the Stack package to use Get_Model in normal code since this breaks our
stack's abstraction.
Here's an example of trying to break that abstraction in the subprogram Peek below.
21 private
22
30 end Stacks;
Prover output
We see that marking the function as Ghost achieves this goal: it ensures that the subprogram
Get_Model is never used in production code.
Though it happens less frequently, you may have specifications requiring you to store additional
information in global variables that isn't needed in normal code. You should mark these global
variables as ghost, allowing the compiler to remove them when assertions aren't enabled. You
can use these variables for any purpose within the contracts that make up your specifications. A
common scenario is writing specifications for subprograms that modify a complex or private global
data structure: you can use these variables to provide a model for that structure that's updated by
the ghost code as the program modifies the data structure itself.
You can also use ghost variables to store information about previous runs of subprograms to spec-
ify temporal properties. In the following example, we have two procedures, one that accesses a
state A and the other that accesses a state B. We use the ghost variable Last_Accessed_Is_A to
specify that B can't be accessed twice in a row without accessing A in between.
15 end Call_Sequence;
3 procedure Access_A is
4 begin
5 -- ...
6 Last_Accessed_Is_A := True;
7 end Access_A;
8
9 procedure Access_B is
10 begin
11 -- ...
12 Last_Accessed_Is_A := False;
13 end Access_B;
14
15 end Call_Sequence;
3 procedure Main is
4 begin
5 Access_A;
6 Access_B;
7 Access_B; -- ERROR
8 end Main;
Prover output
Runtime output
Let's look at another example. The specification of a subprogram's expected behavior is sometimes
best expressed as a sequence of actions it must perform. You can use global ghost variables that
store intermediate values of normal variables to write this sort of specification more easily.
For example, we specify the subprogram Do_Two_Things below in two steps, using the ghost
variable V_Interm to store the intermediate value of V between those steps. We could also express
this using an existential quantification on the variable V_Interm, but it would be impractical to
iterate over all integers at runtime and this can't always be written in SPARK because quantification
is restricted to for ... loop patterns.
Finally, supplying the value of the variable may help the prover verify the contracts.
14 end Action_Sequence;
Prover output
Note: For more details on ghost code, see the SPARK User's Guide23 .
23 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/specification_features.html#
ghost-code
Since properties of interest for functional correctness are more complex than those involved in
proofs of program integrity, we expect GNATprove to initially be unable to verify them even though
they're valid. You'll find the techniques we discussed in Debugging Failed Proof Attempts (page 54) to
come in handy here. We now go beyond those techniques and focus on more ways of improving
results in the cases where the property is valid but GNATprove can't prove it in a reasonable amount
of time.
In those cases, you may want to try and guide GNATprove to either complete the proof or strip it
down to a small number of easily-reviewable assumptions. For this purpose, you can add asser-
tions to break complex proofs into smaller steps.
pragma Assert (Assertion_Checked_By_The_Tool);
-- info: assertion proved
One such intermediate step you may find useful is to try to prove a theoretically-equivalent version
of the desired property, but one where you've simplified things for the prover, such as by splitting
up different cases or inlining the definitions of functions.
Some intermediate assertions may not be proved by GNATprove either because it's missing some
information or because the amount of information available is confusing. You can verify these
remaining assertions by other means such as testing (since they're executable) or by review. You
can then choose to instruct GNATprove to ignore them, either by turning them into assumptions,
as in our example, or by using a pragma Annotate. In both cases, the compiler generates code
to check these assumptions at runtime when you enable assertions.
You can use ghost code to enhance what you can express inside intermediate assertions in the
same way we did above to enhance our contracts in specifications. In particular, you'll commonly
have local variables or constants whose only purpose is to be used in assertions. You'll mostly use
these ghost variables to store previous values of variables or expressions you want to refer to in
assertions. They're especially useful to refer to initial values of parameters and expressions since
the 'Old attribute is only allowed in postconditions.
In the example below, we want to help GNATprove verify the postcondition of P. We do this by
introducing a local ghost constant, X_Init, to represent this value and writing an assertion in both
branches of an if statement that repeats the postcondition, but using X_Init.
13 end Show_Local_Ghost;
3 procedure P (X : in out T) is
4 X_Init : constant T := X with Ghost;
5 begin
6 if Condition (X) then
7 X := X + 1;
8 pragma Assert (F (X, X_Init));
9 else
10 X := X * 2;
11 pragma Assert (F (X, X_Init));
12 end if;
13 end P;
14
15 end Show_Local_Ghost;
Prover output
You can also use local ghost variables for more complex purposes such as building a data structure
that serves as witness for a complex property of a subprogram. In our example, we want to prove
that the Sort procedure doesn't create new elements, that is, that all the elements present in A
after the sort were in A before the sort. This property isn't enough to ensure that a call to Sort
produces a value for A that's a permutation of its value before the call (or that the values are in-
deed sorted). However, it's already complex for a prover to verify because it involves a nesting of
quantifiers. To help GNATprove, you may find it useful to store, for each index I, an index J that
has the expected property.
Ghost procedures can't affect the value of normal variables, so they're mostly used to perform
operations on ghost variables or to group together a set of intermediate assertions.
Abstracting away the treatment of assertions and ghost variables inside a ghost procedure has sev-
eral advantages. First, you're allowed to use these variables in any way you choose in code inside
ghost procedures. This isn't the case outside ghost procedures, where the only ghost statements
allowed are assignments to ghost variables and calls to ghost procedures.
As an example, the for loop contained in Increase_A couldn't appear by itself in normal code.
11 end Show_Ghost_Proc;
3 procedure Increase_A is
4 begin
5 for I in A'Range loop
6 A (I) := A (I) + 1;
7 end loop;
8 end Increase_A;
9
10 end Show_Ghost_Proc;
Prover output
Using the abstraction also improves readability by hiding complex code that isn't part of the func-
tional behavior of the subprogram. Finally, it can help GNATprove by abstracting away assertions
that would otherwise make its job more complex.
In the example below, calling Prove_P with X as an operand only adds P (X) to the proof context
instead of the larger set of assertions required to verify it. In addition, the proof of P need only
be done once and may be made easier not having any unnecessary information present in its con-
text while verifying it. Also, if GNATprove can't fully verify Prove_P, you can review the remaining
assumptions more easily since they're in a smaller context.
When the program involves a loop, you're almost always required to provide additional annotations
to allow GNATprove to complete a proof because the verification techniques used by GNATprove
don't handle cycles in a subprogram's control flow. Instead, loops are flattened by dividing them
into several acyclic parts.
As an example, let's look at a simple loop with an exit condition.
Stmt1;
loop
Stmt2;
exit when Cond;
Stmt3;
end loop;
Stmt4;
The first, shown in yellow, starts earlier in the subprogram and enters the loop statement. The
loop itself is divided into two parts. Red represents a complete execution of the loop's body: an
execution where the exit condition isn't satisfied. Blue represents the last execution of the loop,
which includes some of the subprogram following it. For that path, the exit condition is assumed
to hold. The red and blue parts are always executed after the yellow one.
GNATprove analyzes these parts independently since it doesn't have a way to track how variables
may have been updated by an iteration of the loop. It forgets everything it knows about those
variables from one part when entering another part. However, values of constants and variables
that aren't modified in the loop are not an issue.
In other words, handling loops in that way makes GNATprove imprecise when verifying a subpro-
gram involving a loop: it can't verify a property that relies on values of variables modified inside
the loop. It won't forget any information it had on the value of constants or unmodified variables,
but it nevertheless won't be able to deduce new information about them from the loop.
For example, consider the function Find which iterates over the array A and searches for an ele-
ment where E is stored in A.
7 end Show_Find;
17 end Show_Find;
Prover output
↪precondition]
At the end of each loop iteration, GNATprove knows that the value stored at index I in A must not
be E. (If it were, the loop wouldn't have reached the end of the interation.) This proves the second
assertion. But it's unable to aggregate this information over multiple loop iterations to deduce that
it's true for all the indexes smaller than I, so it can't prove the first assertion.
To overcome these limitations, you can provide additional information to GNATprove in the form
of a loop invariant. In SPARK, a loop invariant is a Boolean expression which holds true at every
iteration of the loop. Like other assertions, you can have it checked at runtime by compiling the
program with assertions enabled.
The major difference between loop invariants and other assertions is the way it's treated for proofs.
GNATprove performs the proof of a loop invariant in two steps: first, it checks that it holds for the
first iteration of the loop and then it checks that it holds in an arbitrary iteration assuming it held
in the previous iteration. This is called proof by induction24 .
As an example, let's add a loop invariant to the Find function stating that the first element of A is
not E.
7 end Show_Find;
16 end Show_Find;
Prover output
To verify this invariant, GNATprove generates two checks. The first checks that the assertion holds
in the first iteration of the loop. This isn't verified by GNATprove. And indeed there's no reason to
expect the first element of A to always be different from E in this iteration. However, the second
check is proved: it's easy to deduce that if the first element of A was not E in a given iteration it's still
not E in the next. However, if we move the invariant to the end of the loop, then it is successfully
verified by GNATprove.
Not only do loop invariants allow you to verify complex properties of loops, but GNATprove also
uses them to verify other properties, such as the absence of runtime errors over both the loop's
body and the statements following the loop. More precisely, when verifying a runtime check or
other assertion there, GNATprove assumes that the last occurrence of the loop invariant preceding
the check or assertion is true.
Let's look at a version of Find where we use a loop invariant instead of an assertion to state that
none of the array elements seen so far are equal to E.
7 end Show_Find;
16 end Show_Find;
Prover output
This version is fully verified by GNATprove! This time, it proves that the loop invariant holds in
every iteration of the loop (separately proving this property for the first iteration and then for the
following iterations). It also proves that none of the elements of A are equal to E after the loop
exits by assuming that the loop invariant holds in the last iteration of the loop.
Note: For more details on loop invariants, see the SPARK User's Guide25 .
Finding a good loop invariant can turn out to be quite a challenge. To make this task easier, let's
review the four good properties of a good loop invariant:
Prop- Description
erty
INIT It should be provable in the first iteration of the loop.
INSIDE It should allow proving the absence of run-time errors and local assertions inside the
loop.
AFTER It should allow proving absence of run-time errors, local assertions, and the subpro-
gram postcondition after the loop.
PRE- It should be provable after the first iteration of the loop.
SERVE
Let's look at each of these in turn. First, the loop invariant should be provable in the first iteration
of the loop (INIT). If your invariant fails to achieve this property, you can debug the loop invari-
ant's initialization like any failing proof attempt using strategies for Debugging Failed Proof Attempts
(page 54).
25 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/assertion_pragmas.html#
loop-invariants
Second, the loop invariant should be precise enough to allow GNATprove to prove absence of run-
time errors in both statements from the loop's body (INSIDE) and those following the loop (AFTER).
To do this, you should remember that all information concerning a variable modified in the loop
that's not included in the invariant is forgotten by GNATprove. In particular, you should take care
to include in your invariant what's usually called the loop's frame condition, which lists properties
of variables that are true throughout the execution of the loop even though those variables are
modified by the loop.
Finally, the loop invariant should be precise enough to prove that it's preserved through successive
iterations of the loop (PRESERVE). This is generally the trickiest part. To understand why GNATprove
hasn't been able to verify the preservation of a loop invariant you provided, you may find it useful
to repeat it as local assertions throughout the loop's body to determine at which point it can no
longer be proved.
As an example, let's look at a loop that iterates through an array A and applies a function F to each
of its elements.
10 end Show_Map;
14 end Show_Map;
Prover output
After the loop, each element of A should be the result of applying F to its previous value. We want
to prove this. To specify this property, we copy the value of A before the loop into a ghost variable,
A_I. Our loop invariant states that the element at each index less than K has been modified in the
expected way. We use the Loop_Entry attribute to refer to the value of A on entry of the loop
instead of using A_I.
Does our loop invariant have the four properties of a good loop-invariant? When launching GNAT-
prove, we see that INIT is fulfilled: the invariant's initialization is proved. So are INSIDE and AF-
TER: no potential runtime errors are reported and the assertion following the loop is successfully
verified.
The situation is slightly more complex for the PRESERVE property. GNATprove manages to prove
that the invariant holds after the first iteration thanks to the automatic generation of frame con-
ditions. It was able to do this because it completes the provided loop invariant with the following
frame condition stating what part of the array hasn't been modified so far:
pragma Loop_Invariant
(for all J in K .. A'Last => A (J) = (if J > K then A'Loop_Entry (J)));
GNATprove then uses both our and the internally-generated loop invariants to prove PRESERVE.
However, in more complex cases, the heuristics used by GNATprove to generate the frame condi-
tion may not be sufficient and you'll have to provide one as a loop invariant. For example, consider
a version of Map where the result of applying F to an element at index K is stored at index K-1:
10 end Show_Map;
20 end Show_Map;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
(continues on next page)
You need to uncomment the second loop invariant containing the frame condition in order to prove
the assertion after the loop.
Note: For more details on how to write a loop invariant, see the SPARK User's Guide26 .
5.4.1 Example #1
We implement a ring buffer inside an array Content, where the contents of a ring buffer of length
Length are obtained by starting at index First and possibly wrapping around the end of the
buffer. We use a ghost function Get_Model to return the contents of the ring buffer for use in
contracts.
13 end Ring_Buffer;
39 end Ring_Buffer;
Prover output
This is correct: Get_Model is used only in contracts. Calls to Get_Model make copies of the buffer's
contents, which isn't efficient, but is fine because Get_Model is only used for verification, not in
production code. We enforce this by making it a ghost function. We'll produce the final produc-
tion code with appropriate compiler switches (i.e., not using -gnata) that ensure assertions are
ignored.
5.4.2 Example #2
Instead of using a ghost function, Get_Model, to retrieve the contents of the ring buffer, we're now
using a global ghost variable, Model.
23 end Ring_Buffer;
20 end Ring_Buffer;
Build output
ring_buffer.adb:8:08: error: ghost entity cannot appear in this context
gprbuild: *** compilation phase failed
Prover output
Phase 1 of 2: generation of Global contracts ...
ring_buffer.adb:8:08: error: ghost entity cannot appear in this context
gnatprove: error during generation of Global contracts
This example isn't correct. Model, which is a ghost variable, must not influence the return value of
the normal function Valid_Model. Since Valid_Model is only used in specifications, we should
have marked it as Ghost. Another problem is that Model needs to be updated inside Push_Last
to reflect the changes to the ring buffer.
5.4.3 Example #3
23 end Ring_Buffer;
22 end Ring_Buffer;
Prover output
This example is correct. The ghost variable Model can be referenced both from the body of the
ghost function Valid_Model and the non-ghost procedure Push_Last as long as it's only used in
ghost statements.
5.4.4 Example #4
We're now modifying Push_Last to share the computation of the new length between the opera-
tional and ghost code.
23 end Ring_Buffer;
23 end Ring_Buffer;
Build output
Prover output
This example isn't correct. We didn't mark local constant New_Length as Ghost, so it can't be
computed from the value of ghost variable Model. If we made New_Length a ghost constant, the
compiler would report the problem on the assignment from New_Length to Length. The correct
solution here is to compute New_Length from the value of the non-ghost variable Length.
5.4.5 Example #5
Let's move the code updating Model inside a local ghost procedure, Update_Model, but still using
a local variable, New_Length, to compute the length.
23 end Ring_Buffer;
19 begin
20 if First + Length <= Max_Size then
21 Content (First + Length) := E;
22 else
23 Content (Length - Max_Size + First) := E;
24 end if;
25 Length := Length + 1;
26 Update_Model;
27 end Push_Last;
28
29 end Ring_Buffer;
Prover output
Everything's fine here. Model is only accessed inside Update_Model, itself a ghost procedure, so
it's fine to declare local variable New_Length without the Ghost aspect: everything inside a ghost
procedure body is ghost. Moreover, we don't need to add any contract to Update_Model: it's
inlined by GNATprove because it's a local procedure without a contract.
5.4.6 Example #6
The function Max_Array takes two arrays of the same length (but not necessarily with the same
bounds) as arguments and returns an array with each entry being the maximum values of both
arguments at that index.
8 end Array_Util;
18 end Array_Util;
Prover output
array_util.adb:13:17: medium: overflow check might fail [reason for check: result␣
↪of addition must fit in a 32-bits machine integer] [possible fix: loop at line 7␣
This program is correct, but GNATprove can't prove that J is always in the index range of B (the
unproved index check) or even that it's always within the bounds of its type (the unproved overflow
check). Indeed, when checking the body of the loop, GNATprove forgets everything about the cur-
rent value of J because it's been modified by previous loop iterations. To get more precise results,
we need to provide a loop invariant.
5.4.7 Example #7
Let's add a loop invariant that states that J stays in the index range of B and let's protect the
increment to J by checking that it's not already the maximal integer value.
8 end Array_Util;
21 end Array_Util;
Prover output
The loop invariant now allows verifying that no runtime error can occur in the loop's body (property
INSIDE seen in section Loop Invariants (page 111)). Unfortunately, GNATprove fails to verify that the
invariant stays valid after the first iteration of the loop (property PRESERVE). Indeed, knowing that
J is in B'Range in a given iteration isn't enough to prove it'll remain so in the next iteration. We
need a more precise invariant, linking J to the value of the loop index I, like J = I - A'First +
B'First.
5.4.8 Example #8
We now consider a version of Max_Array which takes arguments that have the same bounds. We
want to prove that Max_Array returns an array of the maximum values of both its arguments at
each index.
10 end Array_Util;
18 end Array_Util;
3 procedure Main is
4 A : Nat_Array := (1, 1, 2);
5 B : Nat_Array := (2, 1, 0);
6 R : Nat_Array (1 .. 3);
7 begin
8 R := Max_Array (A, B);
9 end Main;
Build output
Prover output
main.adb:8:09: medium: length check might fail [reason for check: array must be of␣
↪the appropriate length]
Runtime output
Here, GNATprove doesn't manage to prove the loop invariant even for the first loop iteration (prop-
erty INIT seen in section Loop Invariants (page 111)). In fact, the loop invariant is incorrect, as you
can see by executing the function Max_Array with assertions enabled: at each loop iteration, R
contains the maximum of A and B only until I - 1 because the I'th index wasn't yet handled.
5.4.9 Example #9
We now consider a procedural version of Max_Array which updates its first argument instead of
returning a new array. We want to prove that Max_Array sets the maximum values of both its
arguments into each index in its first argument.
10 end Array_Util;
17 end Array_Util;
Prover output
Everything is proved. The first loop invariant states that the values of A before the loop index
contains the maximum values of the arguments of Max_Array (referring to the input value of A
with A'Loop_Entry). The second loop invariant states that the values of A beyond and including
the loop index are the same as they were on entry. This is the frame condition of the loop.
10 end Array_Util;
15 end Array_Util;
Prover output
Everything is still proved. GNATprove internally generates the frame condition for the loop, so it's
sufficient here to state that A before the loop index contains the maximum values of the arguments
of Max_Array.