1. Introduction — SPARK Reference Manual 27.0w (original) (raw)

SPARK is a programming language and a set of verification tools designed to meet the needs of high-assurance software development. SPARK is based on Ada, both subsetting the language to remove features that defy verification and also extending the system of contracts by defining new Ada aspects to support modular, constructive, formal verification.

The new aspects support the analysis of incomplete programs, abstraction and refinement and facilitate deep static analysis to be performed including information-flow analysis and formal verification of an implementation against a specification.

Meaningful static analysis is possible on complete programs without the SPARK specific aspects and pragmas (for programs which are otherwise within the SPARK subset), in fact the formal verification of an implementation against a specification of a complete program is possible using only the Ada contracts. Without the SPARK specific aspects, however, analysis has to be performed on a completed program and cannot be applied constructively during its development.

The current version of SPARK, sometimes referred to as SPARK 2014, is a much larger and more flexible language than its predecessor SPARK 2005. The language can be configured to suit a number of application domains and standards, from server-class high-assurance systems to embedded, hard real-time, critical systems.

A major feature of SPARK is the support for a mixture of proof and other verification methods such as testing. This facilitates the use of unit proof in place of unit testing, for example as formalized in avionics certification standard DO-178C and its DO-333 formal methods supplement. Certain units may be formally proven and other units validated through testing.

Ada 2012 introduced executable contracts such as Pre and Post conditions and new types of expression, in particular conditional expressions and quantifiers. SPARK uses these contracts and expressions and extends them with new aspects and pragmas.

The new aspects defined for SPARK all have equivalent pragmas which allows a SPARK program to be compiled by and executed by any Ada implementation; for instance an Ada 95 compiler provided that the use of Ada 2005 and Ada 2012 specific features is avoided. The SPARK attributes Initialized and Loop_Entry can be used only if the Ada implementation supports them.

The direct use of the new aspects requires an Ada 2012 compiler which supports them in a way consistent with the definition given here in the SPARK reference manual. The GNAT implementation is one such compiler.

As with the Ada contracts, the new SPARK aspects and pragmas have executable semantics and may be executed at run time. An expression in an Ada contract or SPARK aspect or pragma is called an_assertion expression_ and it is the ability to execute such expressions which facilitates the mix of proof and testing.

The run-time checking of assertion expressions may be suppressed by using the Ada pragma Assertion_Policy but the static analysis and proof tools always use the assertion expressions whatever the assertion policy.

1.1. Structure of Introduction

This introduction contains the following sections:

1.2. How to Read and Interpret this Manual

This RM (reference manual) is not a tutorial guide to SPARK. It is intended as a reference guide for users and implementors of the language. In this context, “implementors” includes those producing both compilers and verification tools.

This manual is written in the style and language of the Ada RM, so knowledge of Ada is assumed. Chapters 2 through 13 mirror the structure of the Ada RM. Chapters 14 onward cover all the annexes of the Ada RM. Moreover, this manual should be interpreted as an extension of the Ada RM (that is, SPARK is fully defined by this document taken together with the Ada RM).

The SPARK RM uses and introduces technical terms in its descriptions, those that are less well known or introduced are summarized in a Glossary following the sections covering the Ada annexes.

SPARK introduces a number of aspects. The language rules are written as if all the SPARK specific aspects are present but minimum requirements are placed on a tool which analyzes SPARK to be able to synthesize (from the source code) some of these aspects if they are not present. A tool may synthesize more aspects than the minimum required (see Synthesis of SPARK Aspects). An equivalent pragma is available for each of the new aspects but these are not covered explicitly in the language rules either. The pragmas used by SPARK are documented in Language-Defined Pragmas (Annex L).

Readers interested in how SPARK 2005 constructs and idioms map into SPARK should consult the appendix SPARK 2005 to SPARK 2014 Mapping Specification.

1.3. Method of Description

In expressing the aspects, pragmas, attributes and rules of SPARK, the following chapters of this document follow the notational conventions of the Ada RM (section 1.1.4).

The following sections are given for each new language feature introduced for SPARK, following the Ada RM (other than Verification Rules, which is specific to SPARK):

  1. Syntax: this section gives the format of any SPARK specific syntax.
  2. Legality Rules: these are rules that are enforced at compile time. A construct is legal if it obeys all of the Legality Rules.
  3. Static Semantics: a definition of the compile-time effect of each construct.
  4. Dynamic Semantics: a definition of the run-time effect of each construct.
  5. Verification Rules: these rules define checks to be performed on the language feature that relate to static analysis rather than simple legality rules.
  6. Name Resolution Rules: There are very few SPARK specific name resolution rules. Where they exist they are placed under this heading.

A section might not be present if there are no rules specific to SPARK associated with the language feature.

When presenting rules, additional text may be provided in square brackets [ ]. This text is redundant in terms of defining the rules themselves and simply provides explanatory detail.

In addition, examples of the use of the new features are given along with the language definition detail.

1.4. Formal Analysis

SPARK will be amenable to a range of formal analyses, including but not limited to the following static analysis techniques:

Data and information-flow analysis is not valid and might not be possible if the legality rules of Ada and those presented in this document are not met. Similarly, a formal verification might not be possible if the legality rules are not met and may be unsound if data-flow errors are present.

1.4.1. Further Details on Formal Verification

Many Ada constructs have dynamic semantics which include a requirement that some error condition must or may[1] be checked, and some exception must or may[1] be raised, if the error is detected (see Ada RM 1.1.5(5-8)). For example, evaluating the name of an array component includes a check that each index value belongs to the corresponding index range of the array (see Ada RM 4.1.1(7)).

For every such run-time check a corresponding obligation to prove that the error condition cannot be true is introduced. In particular, this rule applies to the run-time checks associated with any assertion (see Ada RM (11.4.2)); the one exception to this rule is pragmaAssume (see Proof Pragmas).

In addition, the generation of verification conditions is unaffected by the suppression of checks (e.g., via pragma Suppress) or the disabling of assertions (e.g., via pragma Assertion_Policy). In other words, suppressing or disabling a check does not prevent generation of its associated verification conditions. Similarly, the verification conditions generated to ensure the absence of numeric overflow for operations of a floating point type T are unaffected by the value of T’Machine_Overflows.

All such generated verification conditions must be discharged before the formal program verification phase may be considered to be complete.

Footnotes

A SPARK implementation has the option of treating any construct which would otherwise generate an unsatisfiable verification condition as illegal, even if the construct will never be executed. For example, a SPARK implementation might reject the declaration

in almost any context. [Roughly speaking, if it can be determined statically that a runtime check associated with some construct will inevitably fail whenever the construct is elaborated, then the implementation is allowed (but not required) to reject the construct just as if the construct violated a legality rule.] For purposes of this rule, the Ada rule that Program_Error is raised if a function “completes normally without executing a return statement” is treated as a check associated with the end of the function body’s sequence_of_statements. [This treatment gives SPARK implementations the option of imposing simpler (but more conservative) rules to ensure that the end of a function is not reachable. Strictly speaking, this rule gives SPARK implementations the option of rejecting many things that should not be rejected (e.g., “pragma Assert (False);” in an unreachable arm of a case statement); reasonable implementations will not misuse this freedom.]

Formal verification of a program may depend on properties of either the machine on which it is to be executed or on properties of the tools used to compile and build it. For example, a program might depend on the bounds of the type Standard.Long_Integer or on the implementation-dependent bounds chosen for the unconstrained base subtype associated with a declaration like “type T is range 1 .. 10;”. In such cases it must be possible to provide the needed information as explicit inputs to the formal verification process. The means by which this is accomplished is not specified as part of the SPARK language definition.

1.5. Executable Contracts and Mathematical Numbers

Contracts, in the form of assertion expressions, are executable in Ada and SPARK and have the same semantics in both. The new aspects and pragmas introduced by SPARK where they are assertion expressions are also executable. Executable contracts have a number of advantages but also a few drawbacks that SPARK to a large extent mitigates.

The Ada pragma Assertion_Policy controls whether contracts and assertion expressions in general are executed and checked at run-time. Assertion expressions are always significant in static analysis and proof and, indeed, form the basis of the specification against which the implementation is verified.

In summary, Ada in itself enables contract-based, dynamic verification of complex properties of a program. SPARK enables contract-based static deductive verification of a large subset of Ada.

1.5.1. The Advantages of Executable Contracts

The possibility of making assertions and contracts executable benefits the programmer in a number of ways:

Executable contracts can be less expressive than pure mathematical ones, or more difficult to write in some situations but SPARK has features to largely mitigate these issues as described in the following subsections.

1.5.2. Mathematical Numbers and Arithmetic

In Ada numeric overflow may occur when evaluating an assertion expression this adds to the complexity of writing contracts and specifications using them, for instance, the expression

Post => X = (Y + Z) / 100

might raise a run-time exception if Y is an integer and Y + Z > Integer’Last even if the entire expression is less then Integer’Last.

SPARK requires checks that have to be proven to demonstrate that an overflow cannot occur, which would not be provable in the above example. Instead, the postcondition would would have to be rewritten, perhaps as something like:

Post => X = Integer ((Long_Integer (Y) + Long_Integer (Z)) / 100)

In general, the Ada library Ada.Numerics.Big_Numbers.Big_Integers can be used so that expressions (at least for Integer types) are treated as mathematical, with no overflow and no exception raised. Using this library, the above example can be rewritten:

Post => To_Big_Integer (X) = (To_Big_Integer (Y) + To_Big_Integer (Z)) / 100

1.5.3. Libraries for Specification and Proof

It is intended that SPARK toolchains have available libraries (as packages) of common paradigms such as sets, supported by an underlying model of the library packages with an expressive specification that makes automatic proof of (executable) contracts using these libraries practical.

1.6. Dynamic Semantics of SPARK Programs

Every valid SPARK program is also a valid Ada program. However, SPARK makes use of SPARK-defined attributes, aspects, and pragmas which an Ada compiler must process consistently with their SPARK definitions in order to compile and execute a SPARK program as an Ada program; this is possible because Ada permits implementation-defined attributes, aspects, and pragmas. The dynamic semantics of SPARK and of Ada are the same, assuming appropriate Ada support for those SPARK-defined constructs. That one sentence defines the dynamic semantics of SPARK; the only other description of dynamic semantics in the SPARK language definition is in defining these SPARK-defined attributes, aspects, and pragmas.

SPARK programs that have failed their static analysis checks can still be valid Ada programs. An incorrect SPARK program with, say, flow analysis anomalies or undischarged verification conditions can still be executed as long as the Ada compiler in question finds nothing objectionable. What one gives up in this case is the formal analysis of the program, such as proof of absence of run-time errors or the static checks performed by flow analysis such as the proof that all variables are initialized before use.

SPARK may make use of certain aspects, attributes and pragmas which are not defined in the Ada reference manual. Ada explicitly permits implementations to provide implementation-defined aspects, attributes and pragmas. If a SPARK program uses one of these aspects (e.g., Global), or attributes (e.g., Initialized) then it can only be compiled and executed by an implementation which supports the construct in a way consistent with the definition given here in the SPARK reference manual.

If the equivalent pragmas are used instead of the implementation-defined aspects and if the use of implementation-defined attributes is avoided, then a SPARK program may be compiled and executed by any Ada implementation (whether or not it recognizes the SPARK pragmas). Ada specifies that unrecognized pragmas are ignored: an Ada compiler that ignores the pragma is correctly implementing the dynamic semantics of SPARK and the SPARK tools will still be able to undertake all their static checks and proofs. If an Ada compiler defines a pragma with the same name as a SPARK specific pragma but has different semantics, then the compilation or execution of the program may fail.

1.7. Main Program

There is no aspect or pragma in SPARK indicating that a subprogram is a main program. Instead it is expected that any implementation of SPARK will have its own mechanism to allow the tools to identify the main program (albeit not within the language itself).

1.8. SPARK Strategic Requirements

The following requirements give the principal goals to be met by SPARK. Some are expanded in subsequent sections within this chapter.

1.9. Explaining the Strategic Requirements

The following sections provide expanded detail on the main strategic requirements.

1.9.1. Principal Language Restrictions

To facilitate formal analyses and verification, SPARK enforces a number of global restrictions to Ada. While these are covered in more detail in the remaining chapters of this document, the most notable restrictions are:

1.9.2. Combining Formal Verification and Testing

There are common reasons for combining formal verification on some part of a codebase and testing on the rest of the codebase:

  1. Formal verification is only applicable to a part of the codebase. For example, it might not be possible to apply the necessary formal verification to Ada code that is not in SPARK.
  2. Formal verification only gives strong enough results on a part of the codebase. This might be because the desired properties cannot be expressed formally, or because proof of these desired properties cannot be sufficiently automated.
  3. Formal verification might be only cost-effective on a part of the codebase. (And it may be more cost-effective than testing on this part of the codebase.)

Since the combination of formal verification and testing cannot guarantee the same level of assurance as when formal verification alone is used, the goal when combining formal verification and testing is to reach a level of confidence at least as good as the level reached by testing alone.

Mixing of formal verification and testing requires consideration of at least the following three issues.

1.9.2.1. Demarcating the Boundary between Formally Verified and Tested Code

Contracts on subprograms provide a natural boundary for this combination. If a subprogram is proved to respect its contract, it should be possible to call it from a tested subprogram. Conversely, formal verification of a subprogram (including absence of run-time errors and contract checking) depends on called subprograms respecting their own contracts, whether these are verified by formal verification or testing.

In cases where the code to be tested is not SPARK, then additional information may be provided in the code – possibly at the boundary – to indicate this (see section In and Out of SPARK for further details).

1.9.2.2. Checks to be Performed at the Boundary

When a tested subprogram T calls a proved subprogram P, then the precondition of P must hold. Assurance that this is true is generated by executing the assertion that P’s precondition holds during the testing of T.

Similarly, when a proved subprogram P calls a tested subprogram T, formal verification will have shown that the precondition of T holds. Hence, testing of T must show that the postcondition of T holds by executing the corresponding assertion. This is a necessary but not necessarily sufficient condition. Dynamically, there is no check that the subprogram has not updated entities not included in the postcondition.

In general, formal verification works by imposing requirements on the callers of proved code, and these requirements should be shown to hold even when formal verification and testing are combined. Any tool set that proposes a combination of formal verification and testing for SPARK should provide a detailed process for doing so, including any necessary additional testing of proof assumptions.

1.9.2.3. Conditions that Apply to the Tested Code

The unit of test and formal verification is a subprogram (the sequence of statements of a package body is regarded as a subprogram). There are several sources of conditions that apply to a tested subprogram:

1.9.2.3.1. Flow analysis of a non-proven subprogram

If a subprogram is in SPARK but is too complex or difficult to prove formally then it still may be flow analyzed which is a fast and efficient process. Flow analysis in the absence of proof has a number of significant benefits as the subprogram implementation is

1.9.2.3.2. Proving properties of a tested subprogram

A tested subprogram which is in SPARK may have properties, such as the absence of run-time exceptions proven even though the full functionality of the subprogram is tested rather than proven. The extent to which proof is performed is controlled using pragma Assume (see Proof Pragmas).

To perform proof of absence of run-time exceptions but not the postcondition of a subprogram a pragma Assume stating the postcondition is placed immediately prior to each exit point from the subprogram (each return statement or the end of the body). Parts of the postcondition may be proved using a similar scheme.

If the proof of absence of one or more run-time exceptions is not proven automatically or takes too long to prove then pragma Assume may be used to suppress the proof of a particular check.

Pragma Assume informs the proof system that the assumed expression is always True and so the prover does not attempt to prove it. In general pragma Assume should be used with caution but it acts as a pragma Assert when the subprogram code is run. Therefore, in a subprogram that is tested it acts as an extra test.

1.9.2.3.3. Conditions on a tested subprogram which is called from a partially proven subprogram

When a subprogram which is to be partially proven calls a tested (but not proven subprogram) then the following conditions must be met by the called subprogram:

A tool set may provide further tools to demonstrate that the Global aspects are satisfied by a non-SPARK subprogram and possibly partially check the postcondition.

1.9.2.3.4. Conditions on a tested subprogram which is calls a proven subprogram

A tested (but not proven) subprogram which calls a proven subprogram must satisfy the following conditions:

1.9.3. Adding Code for Specification and Verification

Often extra entities, such as types, variables and functions may be required only for test and verification purposes. Such entities are termed _ghost_entities and their use is restricted so that they do not affect the functionality of the program. Complete removal of ghost entities has no functional impact on the program.

SPARK supports ghost subprograms, types, objects, and packages. Ghost subprograms may be executable or non-executable. Non-executable ghost subprograms have no implementation and can be used for the purposes of formal verification only. Such functions may have their specification defined within an external proof tool to facilitate formal verification. This specification is outside of the SPARK language and toolset and therefore cannot be checked by the tools. An incorrect definition of function may lead to an unsound proof which is of no use. Ideally any definition will be checked for soundness by the external proof tools.

If the postcondition of a function, F, can be specified in SPARK as F’Result = E, then the postcondition may be recast as the expression of anexpression_function_declaration as shown below:

function F (V : T) return T1 is (E);

The default postcondition of an expression function is F’Result = E making E both the implementation and the expression defining the postcondition of the function. This is useful, particularly for ghost functions, as the expression which acts as the postcondition might not give the most efficient implementation but if the function is a ghost function this might not matter.

1.9.4. Synthesis of SPARK Aspects

SPARK supports a constructive analysis style where all program units require contracts specified by SPARK specific aspects to be provided on their declarations. Under this constructive analysis style, these contracts have to be designed and added at an early stage to assist modular analysis and verification, and then maintained by the user as a program evolves. When the body of a unit is implemented (or modified) it is checked that it conforms to its contract. However, it is mandated that a SPARK analysis tool shall be able to synthesize a conservative approximation of at least a minimum of SPARK specific aspects from the source code of a unit.

Synthesis of SPARK aspects is fundamental to the analysis of pre-existing code where no SPARK specific aspects are provided.

A SPARK analysis tool is required to be capable of synthesizing at least a basic, conservative Global Aspects,Depends Aspects, Refined_Global Aspects,Refined_Depends Aspects, Abstract_State Aspects,Refined_State Aspects, Initializes Aspects andDefault Initial Conditions from either the implementation code or from other SPARK aspects as follows:

The syntheses described above do not include all of the SPARK aspects and nor do the syntheses cover all facets of the aspects. In complex programs where extra or more precise aspects are required they might have to be specified manually.

An analysis tool may provide the synthesis of more aspects and more precise synthesis of the mandatory ones.

Some use cases where the synthesis of aspects is likely to be required are:

1.9.5. In and Out of SPARK

There are various reasons why it may be necessary to combine SPARK and non-SPARK in the same program, such as (though not limited to):

Hence, it must be possible within the language to indicate what parts are (intended to be) in and what parts are (intended to be) out, of SPARK.

The default is to assume none of the program text is in SPARK, although this can be overridden. A new aspect SPARK_Mode is provided, which may be applied to a unit declaration or a unit body, to indicate when a unit declaration or just its body is in SPARK and should be analyzed. If just the body is not in SPARK a SPARK compatible contract may be supplied on the declaration which facilitates the analysis of units which use the declaration. The tools cannot check that the the given contract is met by the body as it is not analyzed. The burden falls on the user to ensure that the contract represents the behavior of the body as seen by the SPARK parts of the program and – if this is not the case – the assumptions on which the analysis of the SPARK code relies may be invalidated.

In general a definition may be in SPARK but its completion need not be.

A finer grain of mixing SPARK and Ada code is also possible by justifying certain warnings and errors. Warnings may be justified at a project, library unit, unit, and individual warning level. Errors may be justifiable at the individual error level or be unsuppressible errors.

Examples of this are:

Hence, SPARK and non-SPARK code may mix at a fine level of granularity. The following combinations may be typical:

Such patterns are intended to allow for mixed-language programming, mixed-verification using different analysis tools, and mixed-verification between formal verification and more traditional testing. A condition for safely combining the results of formal verification with other verification results is that formal verification tools explicitly list the assumptions that were made to produce their results. The proof of a property may depend on the assumption of other user-specified properties (for example, preconditions and postconditions) or implicit assumptions associated with the foundation and hypothesis on which the formal verification relies (for example, initialization of inputs and outputs, or non-aliasing between parameters). When a complete program is formally verified, these assumptions are discharged by the proof tools, based on the global guarantees provided by the strict adherence to a given language subset. No such guarantees are available when only part of a program is formally verified. Thus, combining these results with other verification results depends on the verification of global and local assumptions made during formal verification.

Full details on the SPARK_Mode aspect are given in the SPARK Toolset User’s Guide (Identifying SPARK Code).

1.9.6. Volatile State

A variable or a state abstraction may be specified as external state to indicate that it represents an external communication channel, for instance, to a device or another subsystem. An external variable may be specified as volatile. A volatile state need not have the same value between two reads without an intervening update. Similarly an update of a volatile variable might not have any effect on the internal operation of a program, its only effects are external to the program. These properties require special treatment of volatile variables during flow analysis and formal verification.

SPARK follows the Ada convention that a read of a volatile variable may have an external effect as well as reading the value of the variable. SPARK extends this notion to cover updates of a volatile variable such that an update of a volatile variable may also have some other observable effect. SPARK further extends these principles to apply to state abstractions (see section External State).