Uses of Pragma Annotate GNATprove¶
This appendix lists all the uses of pragma Annotate
for GNATprove.
Pragma Annotate
can also be used to control other AdaCore tools. The uses
of this pragma are explained in the User’s guide of each tool.
The main usage of pragmas Annotate
for GNATprove is for justifying check
messages using Direct Justification with Pragma Annotate. Specific
versions of this pragma can also be used to influence the generation of proof
obligations. Some of these uses can be seen in SPARK Libraries for
example. These forms of pragma Annotate
should be used with care as they
can introduce additional assumptions which are not verified by the GNATprove
tool.
Using Pragma Annotate to Justify Check Messages¶
You can use annotations of the form
pragma Annotate (GNATprove, False_Positive,
"message to be justified", "reason");
to justify an unproved check message that cannot be proved by other means. See
the section Direct Justification with Pragma Annotate for more details
about this use of pragma Annotate
.
Using pragma Annotate to force Proof of Termination¶
SPARK doesn’t usually prove termination of subprograms. You can instruct it do so using annotations of this form:
pragma Annotate (GNATprove, Terminating, Subp_Or_Package_Entity);
See the section Subprogram Termination about details of this use of
pragma Annotate
.
Customize Quantification over Types with the Iterable Aspect¶
In SPARK, it is possible to allow quantification over any container type
using the Iterable
aspect.
This aspect provides the primitives of a container type that will be used to
iterate over its content. For example, if we write:
type Container is private with
Iterable => (First => First,
Next => Next,
Has_Element => Has_Element);
where
function First (S : Set) return Cursor;
function Has_Element (S : Set; C : Cursor) return Boolean;
function Next (S : Set; C : Cursor) return Cursor;
then quantification over containers can be done using the type Cursor
. For
example, we could state:
(for all C in S => P (Element (S, C)))
to say that S
only contains elements for which a property P
holds. For
execution, this expression is translated as a loop using the provided First
,
Has_Element
, and Next
primitives. For proof, it is translated as a logic
quantification over every element of type Cursor
. To restrict the property
to cursors that are actually valid in the container, the provided function
Has_Element
is used. For example, the property stated above becomes:
(for all C : Cursor => (if Has_Element (S, C) then P (Element (S, C)))
Like for the standard Ada iteration mechanism, it is possible to allow
quantification directly over the elements of the container by providing in
addition an Element
primitive to the Iterable
aspect. For example, if
we write:
type Container is private with
Iterable => (First => First,
Next => Next,
Has_Element => Has_Element
Element => Element);
where
function Element (S : Set; C : Cursor) return Element_Type;
then quantification over containers can be done directly on its elements. For example, we could rewrite the above property into:
(for all E of S => P (E))
For execution, quantification over elements of a container is translated as a loop over its cursors. In the same way, for proof, quantification over elements of a container is no more than syntactic sugar for quantification over its cursors. For example, the above property is translated using quantification over cursors :
(for all C : Cursor => (if Has_Element (S, C) then P (Element (S, C)))
Depending on the application, this translation may be too low-level and introduce an unnecessary burden on the automatic provers. As an example, let us consider a package for functional sets:
package Sets with SPARK_Mode is
type Cursor is private;
type Set (<>) is private with
Iterable => (First => First,
Next => Next,
Has_Element => Has_Element,
Element => Element);
function Mem (S : Set; E : Element_Type) return Boolean with
Post => Mem'Result = (for some F of S => F = E);
function Intersection (S1, S2 : Set) return Set with
Post => (for all E of Intersection'Result => Mem (S1, E) and Mem (S2, E))
and (for all E of S1 =>
(if Mem (S2, E) then Mem (Intersection'Result, E)));
Sets contain elements of type Element_Type
. The most basic operation on sets
is membership test, here provided by the Mem
subprogram. Every other
operation, such as intersection here, is then specified in terms of members.
Iteration primitives First
, Next
, Has_Element
, and Element
, that
take elements of a private type Cursor
as an argument, are only provided for
the sake of quantification.
Following the scheme described previously, the postcondition of Intersection
is translated for proof as:
(for all C : Cursor =>
(if Has_Element (Intersection'Result, C) then
Mem (S1, Element (Intersection'Result, C))
and Mem (S2, Element (Intersection'Result, C))))
and
(for all C1 : Cursor =>
(if Has_Element (S1, C1) then
(if Mem (S2, Element (S1, C1)) then
Mem (Intersection'Result, Element (S1, C1)))))
Using the postcondition of Mem
, this can be refined further into:
(for all C : Cursor =>
(if Has_Element (Intersection'Result, C) then
(for some C1 : Cursor =>
Has_Element (S1, C1) and Element (Intersection'Result, C) = Element (S1, C1))
and (for some C2 : Cursor =>
Has_Element (S2, C2) and Element (Intersection'Result, C) = Element (S2, C2)))))
and
(for all C1 : Cursor =>
(if Has_Element (S1, C1) then
(if (for some C2 : Cursor =>
Has_Element (S2, C2) and Element (S1, C1) = Element (S2, C2)))
then (for some C : Cursor => Has_Element (Intersection'Result, C)
and Element (Intersection'Result, C) = Element (S1, C1))))))
Though perfectly valid, this translation may produce complicated proofs,
especially when verifying complex properties over sets. The GNATprove
annotation Iterable_For_Proof
can be used to change the way for ... of
quantification is translated. More precisely, it allows to provide GNATprove
with a Contains function, that will be used for quantification. For example,
on our sets, we could write:
function Mem (S : Set; E : Element_Type) return Boolean;
pragma Annotate (GNATprove, Iterable_For_Proof, "Contains", Mem);
With this annotation, the postcondition of Intersection
is translated in a
simpler way, using logic quantification directly over elements:
(for all E : Element_Type =>
(if Mem (Intersection'Result, E) then Mem (S1, E) and Mem (S2, E)))
and (for all E : Element_Type =>
(if Mem (S1, E) then
(if Mem (S2, E) then Mem (Intersection'Result, E))))
Note that care should be taken to provide an appropriate function contains,
which returns true if and only if the element E
is present in S
. This
assumption will not be verified by GNATprove.
The annotation Iterable_For_Proof
can also be used in another case.
Operations over complex data structures are sometimes specified using operations
over a simpler model type. In this case, it may be more appropriate to translate
for ... of
quantification as quantification over the model’s cursors. As an
example, let us consider a package of linked lists that is specified using a
sequence that allows accessing the element stored at each position:
package Lists with SPARK_Mode is
type Sequence is private with
Ghost,
Iterable => (...,
Element => Get);
function Length (M : Sequence) return Natural with Ghost;
function Get (M : Sequence; P : Positive) return Element_Type with
Ghost,
Pre => P <= Length (M);
type Cursor is private;
type List is private with
Iterable => (...,
Element => Element);
function Position (L : List; C : Cursor) return Positive with Ghost;
function Model (L : List) return Sequence with
Ghost,
Post => (for all I in 1 .. Length (Model'Result) =>
(for some C in L => Position (L, C) = I));
function Element (L : List; C : Cursor) return Element_Type with
Pre => Has_Element (L, C),
Post => Element'Result = Get (Model (L), Position (L, C));
function Has_Element (L : List; C : Cursor) return Boolean with
Post => Has_Element'Result = (Position (L, C) in 1 .. Length (Model (L)));
procedure Append (L : in out List; E : Element_Type) with
Post => length (Model (L)) = Length (Model (L))'Old + 1
and Get (Model (L), Length (Model (L))) = E
and (for all I in 1 .. Length (Model (L))'Old =>
Get (Model (L), I) = Get (Model (L'Old), I));
function Init (N : Natural; E : Element_Type) return List with
Post => length (Model (Init'Result)) = N
and (for all F of Init'Result => F = E);
Elements of lists can only be accessed through cursors. To specify easily the
effects of position-based operations such as Append
, we introduce a ghost
type Sequence
, that is used to represent logically the content of the linked
list in specifications.
The sequence associated to a list can be constructed using the Model
function. Following the usual translation scheme for quantified expressions, the
last line of the postcondition of Init
is translated for proof as:
(for all C : Cursor =>
(if Has_Element (Init'Result, C) then Element (Init'Result, C) = E));
Using the definition of Element
and Has_Element
, it can then be refined
further into:
(for all C : Cursor =>
(if Position (Init'Result, C) in 1 .. Length (Model (Init'Result))
then Get (Model (Init'Result), Position (Init'Result, C)) = E));
To be able to link this property with other properties specified directly on
models, like the postcondition of Append
, it needs to be lifted to iterate
over positions instead of cursors. This can be done using the postcondition of
Model
that states that there is a valid cursor in L
for each position of
its model. This lifting requires a lot of quantifier reasoning from the prover,
thus making proofs more difficult.
The GNATprove Iterable_For_Proof
annotation can be used to provide
GNATprove with a Model function, that will be to translate quantification on
complex containers toward quantification on their model. For example, on our
lists, we could write:
function Model (L : List) return Sequence;
pragma Annotate (GNATprove, Iterable_For_Proof, "Model", Entity => Model);
With this annotation, the postcondition of Init
is translated directly as a
quantification on the elements of the result’s model:
(for all I : Positive =>
(if I in 1 .. Length (Model (Init'Result)) then
Get (Model (Init'Result), I) = E));
Like with the previous annotation, care should be taken to define the model
function such that it always return a model containing exactly the same elements
as L
.
Inlining Functions for Proof¶
Contracts for functions are generally translated by GNATprove has axioms on otherwise undefined functions. As an example, consider the following function:
function Increment (X : Integer) return Integer with
Post => Increment'Result >= X;
It will be translated by GNATprove as follows:
function Increment (X : Integer) return Integer;
axiom : (for all X : Integer. Increment (X) >= X);
For internal reasons due to ordering issues, expression functions are also defined using axioms. For example:
function Is_Positive (X : Integer) return Boolean is (X > 0);
will be translated exactly as if its definition was given through a postcondition, namely:
function Is_Positive (X : Integer) return Boolean;
axiom : (for all X : Integer. Is_Positive (X) = (X > 0));
This encoding may sometimes cause difficulties to the underlying solvers, especially for quantifier instantiation heuristics. This can cause strange behaviors, where an assertion is proven when some calls to expression functions are manually inlined but not without this inlining.
If such a case occurs, it is sometimes possible to instruct the tool to inline
the definition of expression functions using pragma Annotate
Inline_For_Proof
. When such a pragma is provided for an expression
function, a direct definition will be used for the function instead of an
axiom:
function Is_Positive (X : Integer) return Boolean is (X > 0);
pragma Annotate (GNATprove, Inline_For_Proof, Is_Positive);
The same pragma will also allow to inline a regular function, if its postcondition is simply an equality between its result and an expression:
function Is_Positive (X : Integer) return Boolean with
Post => Is_Positive'Result = (X > 0);
pragma Annotate (GNATprove, Inline_For_Proof, Is_Positive);
In this case, GNATprove will introduce a check when verifying the body of
Is_Positive
to make sure that the inline annotation is correct, namely, that
Is_Positive (X)
and X > 0
always yield the same result. This check
may not be redundant with the verification of the postcondition of
Is_Positive
if the =
symbol on booleans has been overridden.
Note that, since the translation through axioms is necessary for ordering
issues, this annotation can sometimes lead to a crash in GNATprove. It is the
case for example when the definition of the function uses quantification over a
container using the Iterable
aspect.