Exceptions
Asserts
When we want to indicate a condition in the code that must always be valid, we
can use the pragma Assert
. As the name implies, when we use this pragma,
we're asserting some truth about the source-code. (We can also use the
procedural form, as we'll see later.)
Important
Another method to assert the truth about the source-code is to use pre and post-conditions.
A simple assert has this form:
In this example, we're asserting that the value of I
is always 10. We
could also display a message if the assertion is false:
Similarly, we can use the procedural form of Assert
. For example, the
code above can implemented as follows:
Note that a call to Assert
is simply translated to a check — and
the Assertion_Error
exception from the Ada.Assertions
package
being raised in the case that the check fails. For example, the code above
roughly corresponds to this:
In the Ada Reference Manual
Assertion policies
We can activate and deactivate assertions based on assertion policies. We can do
that by using the pragma Assertion_Policy
. As an argument to this pragma,
we indicate whether a specific policy must be checked or ignored.
For example, we can deactivate assertion checks by specifying
Assert => Ignore
. Similarly, we can activate assertion checks by
specifying Assert => Check
. Let's see a code example:
Here, we're specifying that asserts shall be ignored. Therefore, the call to the
pragma Assert
doesn't raise an exception. If we replace Ignore
with Check
in the call to Assertion_Policy
, the assert will raise
the Assertion_Error
exception.
The following table presents all policies that we can set:
Policy |
Descripton |
---|---|
|
Check assertions |
|
Check static predicates |
|
Check dynamic predicates |
|
Check pre-conditions |
|
Check pre-condition of classes of tagged types |
|
Check post-conditions |
|
Check post-condition of classes of tagged types |
|
Check type invariants |
|
Check type invariant of classes of tagged types |
In the GNAT toolchain
Compilers are free to include policies that go beyond the ones listed above. For example, GNAT includes the following policies — called assertion kinds in this context:
Assertions
Assert_And_Cut
Assume
Contract_Cases
Debug
Ghost
Initial_Condition
Invariant
Invariant'Class
Loop_Invariant
Loop_Variant
Postcondition
Precondition
Predicate
Refined_Post
Statement_Assertions
Subprogram_Variant
Also, in addtion to Check
and Ignore
, GNAT allows you to set
a policy to Disable
and Suppressible
.
You can read more about them in the GNAT Reference Manual.
You can specify multiple policies in a single call to Assertion_Policy
.
For example, you can activate all policies by writing:
In the GNAT toolchain
With GNAT, policies can be specified in multiple ways. In addition to calls
to Assertion_Policy
, you can use
configuration pragmas files.
You can use these files to specify all pragmas that are relevant to your
application, including Assertion_Policy
. In addition, you can manage
the granularity for those pragmas. For example, you can use a global
configuration pragmas file for your complete application, or even different
files for each source-code file you have.
Also, by default, all policies listed in the table above are deactivated,
i.e. they're all set to Ignore
. You can use the command-line switch
-gnata
to activate them.
Note that the Assert
procedure raises an exception independently of the
assertion policy (Assertion_Policy (Assert => Ignore)
). For example:
Here, the pragma Assert
is ignored due to the assertion policy. However,
the call to Assert
is not ignored.
In the Ada Reference Manual
Checks and exceptions
This table shows all language-defined checks and the associated exceptions:
Check |
Exception |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
In addition, we can use All_Checks
to refer to all those checks above at
once.
Let's discuss each check and see code examples where those checks are performed. Note that all examples are erroneous, so please avoid reusing them elsewhere.
Access Check
As you know, an object of an access type might be null. It would be an error to dereference this object, as it doesn't indicate a valid position in memory. Therefore, the access check verifies that an access object is not null when dereferencing it. For example:
Here, the value of AI
is null by default, so we cannot dereference it.
The access check also performs this verification when assigning to a subtype
that excludes null (not null access
). For example:
Here, the value of AI
is null (by default), so we cannot assign it to
NNAI
because its type excludes null.
Note that, if we remove the := new Integer
assignment from the
declaration of NNAI
, the null exclusion fails in the declaration
itself (because the default value of the access type is null
).
Discriminant Check
As we've seen earlier, a variant record is a record with discriminants that allows for changing its structure. In operations such as an assignment, it's important to ensure that the discriminants of the objects match — i.e. to ensure that the structure of the objects matches. The discriminant check verifies whether this is the case. For example:
Here, R
's discriminant (Valid
) is False
, so we cannot
assign an object whose Valid
discriminant is True
.
Also, when accessing a component, the discriminant check ensures that this component exists for the current discriminant value:
Here, R
's discriminant (Valid
) is False
, so we cannot
access the Counter
component, for it only exists when the Valid
discriminant is True
.
Division Check
The division check verifies that we're not trying to divide a value by zero
when using the /
, rem
and mod
operators. For example:
All three calls in the Show_Division_Check
procedure — to
the Div_Op
, Rem_Op
and Mod_Op
functions — can raise
an exception because we're using 0 as the second argument, which makes the
division check in those functions fail.
Index Check
We use indices to access components of an array. An index check verifies that the index we're using to access a specific component is within the array's bounds. For example:
The range of A_2
— which is passed as an argument to the
Value_Of
function — is 1 to 6. However, in that function call,
we're trying to access position 10, which is outside A_2
's bounds.
Length Check
In array assignments, both arrays must have the same length. To ensure that this is the case, a length check is performed. For example:
Here, the length of Arr_1
is 10, while the length of Arr_2
is 9,
so we cannot assign Arr_2
(From
parameter) to Arr_1
(To
parameter) in the Assign
procedure.
Overflow Check
Operations on scalar objects might lead to overflow, which, if not checked, lead to wrong information being computed and stored. Therefore, an overflow check verifies that the value of a scalar object is within the base range of its type. For example:
In this example, A
already has the last possible value of the
Integer'Base
range, so increasing it by one causes an overflow error.
Range Check
The range check verifies that a scalar value is within a specific range — for instance, the range of a subtype. Let's see an example:
In this example, we're trying to assign 11 to the variable I
of the
Int_1_10
subtype, which has a range from 1 to 10. Since 11 is outside
that range, the range check fails.
Tag Check
The tag check ensures that the tag of a tagged object matches the expected tag in a dispatching operation. For example:
Here, A1
and A2
have different tags:
A1'Tag = T1'Tag
, whileA2'Tag = T2'Tag
.
Since the tags don't match, the tag check fails in the assignment of A1
to A2
.
Accessibility Check
The accessibility check verifies that the accessibility level of an entity matches the expected level.
Todo
Add link to "Accessibility levels" section once it's available.
Let's look at an example that mixes access types and anonymous access types.
Here, we use an anonymous access type in the declaration of A1
and a
named access type in the declaration of A2
:
The anonymous type (access T'Class
), which is used in the declaration of
A1
, doesn't have the same accessibility level as the T_Class
type. Therefore, the accessibility check fails during the T_Class (A1)
conversion.
We can see the accessibility check failing in this example as well:
Again, the check fails in the T_Class (A)
conversion and raises a
Program_Error
exception.
Allocation Check
The allocation check ensures, when a task is about to be created, that its master has not been completed or the finalization has not been started.
This is an example adapted from AI-00280:
Here, in the finalization of the X1
object of T1
type, we're
trying to create an object of T2
type. This is forbidden and, therefore,
the allocation check raises a Program_Error
exception.
Elaboration Check
The elaboration check verifies that subprograms — or protected entries, or task activations — have been elaborated before being called.
This is an example adapted from AI-00064:
This is a curious example: first, we declare a function F
and assign the
value returned by this function to constant Y
in its declaration. Then,
we declare F
as a renamed function, thereby providing a body to F
— this is called renaming-as-body. Consequently, the compiler doesn't
complain that a body is missing for function F
. (If you comment out the
function renaming, you'll see that the compiler can then detect the missing
body.) Therefore, at runtime, the elaboration check fails because the body of
the first declaration of the F
function is actually missing.
Storage Check
The storage check ensures that the storage pool has enough space when allocating memory. Let's revisit an example that we discussed earlier:
On each allocation (new UInt_7
), a storage check is performed. Because
there isn't enough reserved storage space before the second allocation, the
checks fails and raises a Storage_Error
exception.
In the Ada Reference Manual
Ada.Exceptions
package
Note
Parts of this section were originally published as Gem #142 : Exception-ally
The standard Ada run-time library provides the package Ada.Exceptions
.
This package provides a number of services to help analyze exceptions.
Each exception is associated with a (short) message that can be set by the code that raises the exception, as in the following code:
raise Constraint_Error with "some message";
Historically
Since Ada 2005, we can use the
raise Constraint_Error with "some message"
syntax.
In Ada 95, you had to call the Raise_Exception
procedure:
Ada.Exceptions.Raise_Exception -- Ada 95
(Constraint_Error'Identity, "some message");
In Ada 83, there was no way to do it at all.
The new syntax is now very convenient, and developers should be encouraged to provide as much information as possible along with the exception.
In the GNAT toolchain
The length of the message is limited to 200 characters by default in GNAT, and messages longer than that will be truncated.
In the Ada Reference Manual
Retrieving exception information
Exceptions also embed information set by the run-time itself that can be
retrieved by calling the Exception_Information
function. The function
Exception_Information
also displays the Exception_Message
.
For example:
exception
when E : others =>
Put_Line (Ada.Exceptions.Exception_Information (E));
In the GNAT toolchain
In the case of GNAT, the information provided by an exception might include the source location where the exception was raised and a nonsymbolic traceback.
You can also retrieve this information individually. Here, you can use:
the
Exception_Name
functions — and its derivativesWide_Exception_Name
andWide_Wide_Exception_Name
— to retrieve the name of an exception.the
Exception_Message
function to retrieve the message associated with an exception.
Let's see a complete example:
Collecting exceptions
Save_Occurrence
You can save an exception occurrence using the Save_Occurrence
procedure.
(Note that a Save_Occurrence
function exists as well.)
For example, the following application collects exceptions into a list and
displays them after running the Test_Exceptions
procedure:
In the Save_To_List
procedure of the Exception_Tests
package, we
call the Save_Occurrence
procedure to store the exception occurence to
the Occurrences
array. In the Show_Exception_Info
, we display all
the exception occurrences that we collected.
Read
and Write
attributes
Similarly, we can use files to read and write exception occurences. To do that,
we can simply use the 'Read
and 'Write
attributes.
In this example, we store the exceptions raised in the application in the
exceptions_file.bin file. In the exception part of procedures Nested_1
and Nested_2
, we call Exception_Occurrence'Write
to store an
exception occurence in the file. In the Read_Exceptions
block, we read
the exceptions from the the file by calling Exception_Occurrence'Read
.
Debugging exceptions in the GNAT toolchain
Here is a typical exception handler that catches all unexpected exceptions in the application:
The output we get when running the application is not very informative. To get
more information, we need to rerun the program in the debugger. To make the
session more interesting though, we should add debug information in the
executable, which means using the -g
switch in the
gnatmake command.
The session would look like the following (omitting some of the output from the debugger):
> rm *.o # Cleanup previous compilation
> gnatmake -g main.adb
> gdb ./main
(gdb) catch exception
(gdb) run
Catchpoint 1, CONSTRAINT_ERROR at 0x0000000000402860 in main.nested () at main.adb:8
8 raise Constraint_Error with "some message";
(gdb) bt
#0 <__gnat_debug_raise_exception> (e=0x62ec40 <constraint_error>) at s-excdeb.adb:43
#1 0x000000000040426f in ada.exceptions.complete_occurrence (x=x@entry=0x637050)
at a-except.adb:934
#2 0x000000000040427b in ada.exceptions.complete_and_propagate_occurrence (
x=x@entry=0x637050) at a-except.adb:943
#3 0x00000000004042d0 in <__gnat_raise_exception> (e=0x62ec40 <constraint_error>,
message=...) at a-except.adb:982
#4 0x0000000000402860 in main.nested ()
#5 0x000000000040287c in main ()
And we now know exactly where the exception was raised. But in fact, we could
have this information directly when running the application. For this, we need
to bind the application with the switch -E
, which tells the
binder to store exception tracebacks in exception occurrences. Let's recompile
and rerun the application.
> rm *.o # Cleanup previous compilation
> gnatmake -g main.adb -bargs -E
> ./main
Exception name: CONSTRAINT_ERROR
Message: some message
Call stack traceback locations:
0x10b7e24d1 0x10b7e24ee 0x10b7e2472
The traceback, as is, is not very useful. We now need to use another tool that is bundled with GNAT, called addr2line. Here is an example of its use:
> addr2line -e main --functions --demangle 0x10b7e24d1 0x10b7e24ee 0x10b7e2472
/path/main.adb:8
_ada_main
/path/main.adb:12
main
/path/b~main.adb:240
This time we do have a symbolic backtrace, which shows information similar to what we got in the debugger.
For users on OSX machines, addr2line does not exist. On these machines, however, an equivalent solution exists. You need to link your application with an additional switch, and then use the tool atos, as in:
> rm *.o
> gnatmake -g main.adb -bargs -E -largs -Wl,-no_pie
> ./main
Exception name: CONSTRAINT_ERROR
Message: some message
Call stack traceback locations:
0x1000014d1 0x1000014ee 0x100001472
> atos -o main 0x1000014d1 0x1000014ee 0x100001472
main__nested.2550 (in main) (main.adb:8)
_ada_main (in main) (main.adb:12)
main (in main) + 90
We will now discuss a relatively new switch of the compiler, namely
-gnateE
. When used, this switch will generate extra
information in exception messages.
Let's amend our test program to:
When running the application, we see that the exception information (traceback)
is the same as before, but this time the exception message is set automatically
by the compiler. So we know we got a Constraint_Error
because an
incorrect index was used at the named source location
(main.adb
, line 10). But the significant addition is the second
line of the message, which indicates exactly the cause of the error. Here, we
wanted to get the element at index 3, in an array whose range of valid indexes
is from 1 to 2. (No need for a debugger in this case.)
The column information on the first line of the exception message is also very useful when dealing with null pointers. For instance, a line such as:
A := Rec1.Rec2.Rec3.Rec4.all;
where each of the Rec
is itself a pointer, might raise
Constraint_Error
with a message "access check failed". This indicates for
sure that one of the pointers is null, and by using the column information it is
generally easy to find out which one it is.
Exception renaming
We can rename exceptions by using the an exception renaming declaration in this
form Renamed_Exception : exception renames Existing_Exception;
. For
example:
Exception renaming creates a new view of the original exception. If we rename an
exception from package A
in package B
, that exception will become
visible in package B
. For example:
Here, we're renaming the Int_E
exception in the Test_Constraints
package. The Int_E
exception isn't directly visible in the
Show_Exception_Renaming
procedure because we're not with
ing the
Internal_Exceptions
package. However, it is indirectly visible
in that procedure via the renaming (Ext_E
) in the Test_Constraints
package.
In the Ada Reference Manual
Out and Uninitialized
Note
This section was originally written by Robert Dewar and published as Gem #150: Out and Uninitialized
Perhaps surprisingly, the Ada standard indicates cases where objects passed to
out
and in out
parameters might not be updated when a procedure
terminates due to an exception. Let's take an example:
This program outputs a value of 0 for B
, whereas the code indicates that
A
is assigned before raising the exception, and so the reader might
expect B
to also be updated.
The catch, though, is that a compiler must by default pass objects of
elementary types (scalars and access types) by copy and might choose to do so
for other types (records, for example), including when passing out
and
in out
parameters. So what happens is that while the formal parameter
A
is properly initialized, the exception is raised before the new value
of A
has been copied back into B
(the copy will only happen on a
normal return).
In the GNAT toolchain
In general, any code that reads the actual object passed to an out
or
in out
parameter after an exception is suspect and should be avoided.
GNAT has useful warnings here, so that if we simplify the above code to:
We now get a compilation warning that the pass-by-copy formal may have no effect.
Of course, GNAT is not able to point out all such errors (see first example above), which in general would require full flow analysis.
The behavior is different when using parameter types that the standard mandates be passed by reference, such as tagged types for instance. So the following code will work as expected, updating the actual parameter despite the exception:
In the GNAT toolchain
It's worth mentioning that GNAT provides a pragma called
Export_Procedure
that forces reference semantics on out
parameters. Use of this pragma would ensure updates of the actual parameter
prior to abnormal completion of the procedure. However, this pragma only
applies to library-level procedures, so the examples above have to be
rewritten to avoid the use of a nested procedure, and really this pragma is
intended mainly for use in interfacing with foreign code. The code below
shows an example that ensures that B
is set to 1 after the call to
Local
:
In the case of direct assignments to global variables, the behavior in the
presence of exceptions is somewhat different. For predefined exceptions, most
notably Constraint_Error
, the optimization permissions allow some
flexibility in whether a global variable is or is not updated when an exception
occurs (see
Ada RM 11.6). For
instance, the following code makes an incorrect assumption:
X := 0; -- about to try addition
Y := Y + 1; -- see if addition raises exception
X := 1 -- addition succeeded
A program is not justified in assuming that X = 0
if the addition raises
an exception (assuming X
is a global here). So any such assumptions in a
program are incorrect code which should be fixed.
In the Ada Reference Manual
Suppressing checks
pragma Suppress
Note
This section was originally written by Gary Dismukes and published as Gem #63: The Effect of Pragma Suppress.
One of Ada's key strengths has always been its strong typing. The language imposes stringent checking of type and subtype properties to help prevent accidental violations of the type system that are a common source of program bugs in other less-strict languages such as C. This is done using a combination of compile-time restrictions (legality rules), that prohibit mixing values of different types, together with run-time checks to catch violations of various dynamic properties. Examples are checking values against subtype constraints and preventing dereferences of null access values.
At the same time, Ada does provide certain "loophole" features, such as
Unchecked_Conversion
, that allow selective bypassing of the normal
safety features, which is sometimes necessary when interfacing with hardware or
code written in other languages.
Ada also permits explicit suppression of the run-time checks that are there to
ensure that various properties of objects are not violated. This suppression
can be done using pragma Suppress
, as well as by using a compile-time
switch on most implementations — in the case of GNAT, with the -gnatp
switch.
In addition to allowing all checks to be suppressed, pragma Suppress
supports suppression of specific forms of check, such as Index_Check
for
array indexing, Range_Check
for scalar bounds checking, and
Access_Check
for dereferencing of access values. (See section 11.5 of
the Ada Reference Manual for further details.)
Here's a simple example of suppressing index checks within a specific subprogram:
procedure Main is
procedure Sort_Array (A : in out Some_Array) is
pragma Suppress (Index_Check); -- eliminate check overhead
begin
...
end Sort_Array;
end Main;
Unlike a feature such as Unchecked_Conversion
, however, the purpose of
check suppression is not to enable programs to subvert the type system, though
many programmers seem to have that misconception.
What's important to understand about pragma Suppress
is that it only
gives permission to the implementation to remove checks, but doesn't require
such elimination. The intention of Suppress
is not to allow bypassing of
Ada semantics, but rather to improve efficiency, and the Ada Reference Manual
has a clear statement to that effect in the note in RM-11.5, paragraph 29:
There is no guarantee that a suppressed check is actually removed; hence a
pragma Suppress
should be used only for efficiency reasons.
There is associated Implementation Advice that recommends that implementations should minimize the code executed for checks that have been suppressed, but it's still the responsibility of the programmer to ensure that the correct functioning of the program doesn't depend on checks not being performed.
There are various reasons why a compiler might choose not to remove a check. On some hardware, certain checks may be essentially free, such as null pointer checks or arithmetic overflow, and it might be impractical or add extra cost to suppress the check. Another example where it wouldn't make sense to remove checks is for an operation implemented by a call to a run-time routine, where the check might be only a small part of a more expensive operation done out of line.
Furthermore, in many cases GNAT can determine at compile time that a given run-time check is guaranteed to be violated. In such situations, it gives a warning that an exception will be raised, and generates code specifically to raise the exception. Here's an example:
X : Integer range 1..10 := ...;
..
if A > B then
X := X + 1;
..
end if;
For the assignment incrementing X
, the compiler will normally generate
machine code equivalent to:
Temp := X + 1;
if Temp > 10 then
raise Constraint_Error;
end if;
X := Temp;
If range checks are suppressed, then the compiler can just generate the
increment and assignment. However, if the compiler is able to somehow prove
that X = 10
at this point, it will issue a warning, and replace the
entire assignment with simply:
raise Constraint_Error;
even though checks are suppressed. This is appropriate, because
we don't care about the efficiency of buggy code, and
there is no "extra" cost to the check, because if we reach that point, the code will unconditionally fail.
One other important thing to note about checks and pragma Suppress
is
this statement in the Ada RM (RM-11.5, paragraph 26):
If a given check has been suppressed, and the corresponding error situation occurs, the execution of the program is erroneous.
In Ada, erroneous execution is a bad situation to be in, because it means that the execution of your program could have arbitrary nasty effects, such as unintended overwriting of memory. Note also that a program whose "correct" execution somehow depends on a given check being suppressed might work as the programmer expects, but could still fail when compiled with a different compiler, or for a different target, or even with a newer version of the same compiler. Other changes such as switching on optimization or making a change to a totally unrelated part of the code could also cause the code to start failing.
So it's definitely not wise to write code that relies on checks being removed. In fact, it really only makes sense to suppress checks once there's good reason to believe that the checks can't fail, as a result of testing or other analysis. Otherwise, you're removing an important safety feature of Ada that's intended to help catch bugs.
pragma Unsuppress
We can use pragma Unsuppress
to reverse the effect of a
pragma Suppress
. While pragma Suppress
gives permission to the
compiler to remove a specific check, pragma Unsuppress
revokes that
permission.
Let's see an example:
In this example, we first use a pragma Suppress (Index_Check)
, so the
compiler is allowed to remove the index check from the
Unchecked_Value_Of
function. (Therefore, depending on the compiler, the
call to the Unchecked_Value_Of
function may complete without raising an
exception.) Of course, in this specific example, suppressing the index check
masks a severe issue.
In contrast, an index check is performed in the Value_Of
function
because of the pragma Unsuppress
. As a result, the index checks fails in
the call to this function, which raises a Constraint_Error
exception.
In the Ada Reference Manual