Skip to content

Structure#

Control Flow#

If Statements and Expressions#

if I = 10 then
   -- ...
elsif I in 11 .. 20 then -- Note the elsif spelling
   -- ...
else
   -- ...
end if;

-- If expressions must be enclosed in parentheses
If_Expr1: Boolean := (if I > 10 then 1 else 0);
If_Expr2: Boolean := (if I > 1000 then 3 elsif I > 100 then 2 else 0);

-- Special rule for boolean expressions
-- These two are equivalent:
(if C1 then C2 else True)
(if C1 then C2)
-- Mostly useful for preconditions
Pre => (if C1 > 0 then C2 > 0)
-- These two expressions have the same value
(if C1 then C2)
(not C1 or C2)

Case Statements and Expressions#

TODO 7.2

Hours: Integer := (case D is
                   when Mon .. Thurs => 8,
                   when Fri          => 6,
                   when sat | Sun    => 0);
Month_Days: Integer := (case Month is
                        when Sep | Apr | Jun | Nov => 30,
                        when Feb =>
                           (if Year mod 100 = 0 or else
                               Year mod 400 = 0 or else
                               Year mod 4 = 0
                            then 29
                            else 28),
                        when others => 31);

Inline Expressions#

Quantified Expressions#

Quantified expressions are a kind of for loop expression that always return a boolean. They were introduced primarily for preconditions and postconditions.

-- If An_Array'Range is null then it evaluates to True
Are_All_Zero: Boolean := (for all I in An_Array'Range => An_Array(I) = 0);
Are_All_Zero_2D: Boolean :=
   (for all J in A_2D_Array'Range(1) =>
      (for all I in A_2D_Array'Range(2) => A_2D_Array(J, I) = 0));
Are_All_Zero_2D_Iter: Boolean := (for all E of A_2D_Array => E = 0); -- iterator form

-- If An_Array'Range is null then it evaluates to False
Are_Any_Zero: Boolean := (for some I in An_Array'Range => An_Array(I) = 0);

-- Two possible ways of checking if N is a prime
RN: Integer := Integer(Sqrt(Float(N)));
if (for some K in 2 .. RN => N mod K = 0) then -- ...
if (for all K in 2 .. RN => N mod K / = 0) then -- ...

Declaration Expressions#

This is only for Ada2022

For declaration expressions, the values must be constant and not limited, aliased, Access, or Unchecked_Access.

Val1: Float := (declare X: constant Float := A + B;
                  begin X**3 + X**2 + X);
Val2: Float := (declare X renames Float'(A + B); -- Specifying the subtype is not needed in Ada2022
                  begin X**3 + X**2 + X);

Reduction Expressions#

This is only for Ada2022

Reduction expressions may be written using the 'Reduce attribute which takes two arguments: the operation and the starting value for the accumulation. The loop may not contain reverse and an array literal must use brackets.

Range_Sum: Integer   := ([for I in 1 .. 10 => I]'Reduce("+", 0));    -- 55
Squares_Sum: Integer := ([for I in 1 .. 10 => I**2]'Reduce("+", 0)); -- 385

function Factorial(N: Natural) return Natural is
   ([for I in 1 .. N => I]'Reduce("*", 1));

-- Can be written via iterator syntax:
Total_1: Float := [for E of Float_2D_Array => E]'Reduce("+", 0.0);
-- Which can be abbreviated to:
Total_2: Float := Float_2D_Array'Reduce("+", 0.0);

Maximum: Float := Float_2D_Array'Reduce(Float'Max, 0.0);

Scope and Visibility#

Scope is typical in that each block introduces a new scope and creating symbols with a name existing in a parent scope hides the parent’s symbol. In order to access the parent’s symbol after it is hidden, the name of the outer scope can be used. Scopes that do not have names may be given names in order to refer to them for this purpose.

Outer: -- Naming this declare block
declare
   X: Integer := 77;
begin
   -- ...
   for X in 1 .. 10 loop
      Put(Integer'Image(X));      
      Put(Integer'Image(Outer.X));
   end loop; -- 1 77 2 77 3 77 ...
end Outer;

In addition, return can be used to terminate the execution of a subprogram, exit can be used to terminate the execution of the current loop (or a named exit Outer to exit an outer loop) and goto may be used to jump to a label within the current subprogram.

Outer_Loop:
for I in 1 .. Integer'Last loop
   for I in 1 .. 2 loop
      Put(Integer'Image(I));
      Put_Line(Integer'Image(Outer_Loop.I));
   end loop;
   if I = 2 then
      goto label;
   end if;
end loop Outer_Loop;
-- ...
<<label>>

-- 1 1
-- 2 1
-- 1 2
-- 2 2

Subprograms#

Subprograms in Ada are distinguished by functions and procedures. Functions return a value while procedures do not.

Functions

Should a function run into the final end, a Program_Error exception is raised. For procedures, running into the final end is the same as returning.

Functions#

function Factorial(N: Positive) return Positive is
   -- This is the declaration section
begin
   -- This is the statement section where the body of the function goes.
   -- A value must be returned because this is a function.
   if N = 1 then
      return 1;
   else
      return N * Factorial(N - 1);
   end if;
end Factorial;
There are several things which could go wrong in this function:

  1. If an invalid value (such as -1) is passed in, it will result in a Constraint_Error exception.
  2. If a moderate value (such as 100) is passed in, it will result in a Constraint_Error exception for integer overflow.
  3. If a large value (such as 1_000_000) is passed in, it may result in a Storage_Error exception for a stack overflow.

Abbreviated Form#

An abbreviated form of a function can be just an expression:

function Dot(V, U: Vector2) return Float is
   (V.X*U.X + V.Y*U.Y); -- Expression must be in parentheses

Procedures#

-- Parameters with the same type may be grouped
procedure My_Procedure(Param1, Param2: Integer; Param3: Float) is
   -- Local subprograms may also be declared here.
   function Calc_Something(X: Float) return Float is
   begin
      return X + X**2;
   end Calc_Something;

   Val: Float;
begin
   -- Procedure body goes here and does not return a value. It may not be empty.
   Val := Calc_Something(Param3);
   -- ...
end My_Procedure;

Null Procedures#

One special kind of procedure is a null procedure.

procedure Option(X: in T) is null;
-- Null procedures may not have bodies and act as follows:
-- procedure Option(X: in T) is
-- begin
--    null;
-- end Option;

Overriding#

Derived type may overload inherited procedures. There are two optional qualifiers that may be given to subprograms: overriding and not overriding. Although they are optional, using them can prevent certain errors as they require/do not require the subprogram to be currently available.

overriding procedure Option(X: in S);
not overriding procedure Maybe(X: in S);

Parameters#

Parameters may have a combination of two different qualifications: in and out. in parameters are the default.

Function Parameters

Before Ada2012, functions could only take in parameters.

(Param:        Integer) -- Defaults to in
(Param: in     Integer) -- Equivalent to the previous
(Param:    out Integer)
(Param: in out Integer)

in parameters can be passed in as any valid expression and they act as local constants within the subprogram. out and in out parameters can be modified and a variable to be modified must be passed into the subprogram. Parameters marked as out may be copied (depending on the implementation for performance or simplicity reasons), but access types, discriminated record types and records with components that have default initial values are always copied.

For in parameters, scalar types are passed by value, whereas parameters of:

  • arrays
  • task and protected types
  • tagged records
  • explicitly limited types

are always passed by reference (including records containing any of these types).

Type conversions are allowed for arguments and follow the process of converting, processing with the subprogram and then converting back.

-- procedure Increment(I: in out Integer)
A: Float := 1.1;
B: Float := 1.5;
Increment(Integer(A)); -- A is now 2.0; 1.1 -> [1 -> 2] -> 2.0
Increment(Integer(B)); -- B is now 3.0; 1.5 -> [2 -> 3] -> 3.0

Named arguments are given in the same way as named values for records (and similar to that of arrays). The rules for positional arguments are like those of other languages:

  • Positional arguments must come first
  • Named arguments may be in any order
  • Fields with default arguments may still be excluded
-- procedure Put_Line(File: File_Type; Item: String)
Put_Line(File => Standard_Error, Item => "Output line to stderr");

-- procedure Replace_Element(Container: in out Vector; Index: Index_Type; New_Item: Element_Type);
Replace_Element(My_Container, New_Item => My_Item, Index => 10);

Default values may be given for any parameters, but placing them after parameters without default values is not useful unless used with named arguments.

-- procedure My_Procedure(X: Float; Y: Float := 2.0; Z: Float)
My_Procedure(2.0, 4.0, 8.0);
My_Procedure(2.0, 4.0);           -- error: missing argument for parameter "Z"
My_Procedure(X => 2.0, Z => 4.0); -- Okay

Mutable Parameters

For understanding semantics, it is best to think of out parameters as being copied into their destination at the end of the subprogram rather than as implicit pointers. This makes the aliasing rules easier to remember.

Out Parameters

Out parameters that are not assigned to may have undefined values.

procedure Calculate(A: in Float; B: out Float) is
begin
   if A < 0.0 then
      return;
   else
      B := 10.0;
   end if;
end Calculate;

Result: Float;
Calculate(-1.0, Result); -- Result is not given a value and so is still undefined
Changing the subprogram definition to procedure Calculate(A: in Float; B: in out Float) in this case would lead to the possiblity that Result via B is referenced before assignment inside of the subprogram. Both cases (although they may compile) are incorrect and your compiler should emit a warning about this.

A solution in this case (apart from changing the subprogram’s design) is to use a type with a default value: type Real is new Float with Default_Value => 0.0;. Ada’s access types are like this because they default to null to avoid the potential of garbage in a pointer.

Overloading#

Subprograms may be overloaded as in other languages. One difference is that functions and procedures may be overloaded by the return value.

procedure My_SR(X: Float) is 
begin
   -- ...
end My_SR;
    
function My_SR(X: Float) return Integer is 
begin
   -- ...
   return 1;
end My_SR;

My_SR(2.47);               -- Calls the procedure
X: Integer := My_SR(3.69); -- Calls the function

Extended Return Statement#

type Vector is array(Integer range <>) of Float;

function Squared(Input: Vector) return Vector is
begin
   return Result: Vector(Input'Range) do -- Can also provide a default value, if needed
      for I in Input'Range loop
         Result(I) := Input(I) * Input(I);
      end loop;
   end return;
end Squared;

Operators#

The name of a function may be an operator provided in quotation marks. The list of operators that may be overloaded is:

abs and mod not or rem xor
= /= < <= >= >
+ - * / ** &

Operator Overloading

Overloaded operators may not have parameters other than in parameters regardless of Ada version

function "+"(V, U: Vector2) return Vector2 is
   (X => V.X + U.X,
    Y => V.Y + U.Y);

-- Unary sum operator for a vector
-- A_Sum := +A_Vector;
function "+"(V: Vector) return Float is
   Result: Float := 0.0;
begin
   for I in V'Range loop
      Result := Result + V(I);
   end loop;
   return Result;
end "+";

The only operator with special rules is /=. An overload of = need not return a boolean value, but if it does then an overload of /= is implicitly created. If = does not return a boolean value, the /= function may be overloaded as normal.

Membership tests via in and not in and the short-circuit forms of and then and or else may not be given new meanings.

Packages#

Packages are the global namespacing abstraction in Ada. For the GNAT toolchain, the specifications and bodies for packages are normally separated into “Package_Name.ads” specification files and “Package_Name.adb” body files. The only bodies allowed inside of a specification are short abbreviated form functions and any declared as such do not need to appear in the body.

-- Specification
-- For GNAT, this should be in "My_Package.ads".
package My_Package is
   -- Visible values declared here are:
   -- 1. Types
   -- 2. Subprograms
   -- 3. Variables
   -- No bodies are allowed for subprograms, except for functions of the
   -- abbreviated (single expression) form.
private -- Optional section
   -- Symbols declared as "private" are defined here
end My_Package;
-- Implementation
-- For GNAT, this should be in "My_Package.adb".
package body My_Package is
   -- Subprograms declared in the specification must match and have their bodies here.
   -- All symbols in the specification are visible here, but anything declared here
   -- and not in the specification will only be available in this package.
begin -- Optional section
   -- Initialisation statements can go here.
end My_Package;
Accessing symbols inside of packages is done via dot syntax. To add packages into the current scope, with is used and to add symbols from the packages into the current scope, use is used. So, for the package Ada.Text_IO:
with Ada.Text_IO; -- package "Ada.Text_IO" is now available in the current scope.
Ada.Text_IO.Put_Line("Had to access this via dot syntax.");
use Ada.Text_IO;  -- The package's symbols are now in the current scope.
Put_Line("Don't have to use dot syntax now unless there are ambiguities.");
Ada.Text_IO.Put_Line("This still works.");

-- Types and their primitive operators can be made available without "using" the whole package.
use type A_Package.A_Type;
-- To make all operators (including user-defined ones) and not just the primitive ones available:
use all type A_Package.A_Type;
Note that a with statement may only appear in a context clause, but use statements may appear in a declaration clause.

Child Packages#

Child packages may be created by using the dot notation.

-- For GNAT, this should be in "parent_package-child_package.ads"
package Parent_Package.Child_Package is
   -- ...
end Parent_Package.Child_Package;
This child package’s private part has full knowledge of private symbols in its parent package and it does not need a with clause or use clause for its parent. Sibling packages are like unrelated packages except that they may refer to each other through the implicit knowledge of their parent package. The parent’s body may depend on a child, but it must be explicitly included.

Child packages may also be labelled as private (private Parent_Package.Child_Package is -- ...) in which case they are only visible to their parent and siblings and you can use private with Child_Package; in order to only make the package available to the private section of the package. In this case a use statement must be inside the private section as well.

Subunits#

Bodies of subprograms may be removed from their package by making them separate.

package body My_Package is
   procedure SR(X: Integer) is separate;
end My_Package;
These removed units are termed subunits and may be compiled separately. In their definition, they must be preceded by separate followed by the name of the parent package in parentheses.
separate(My_Package)
procedure SR(X: Integer) is
begin
   -- ...
end My_Package;

Mutually Dependent Packages#

When you need packages that depend on each others types, limited with Some_Package; can be used to include the types as though they were declared as incomplete. This allows the typical use of incomplete types such as having access types to them. Using a limited clause does not create a dependency like a standard with clause which means that there is no requirement that Some_Package be compiled before the dependent package.

limited with clauses may only appear in specifications and a use clause cannot apply to packages with a limited view.

Exceptions#

Package

Note

Exceptions are not like normal values. They cannot be in arrays, components of records or parameters of a subprogram. They behave like literals of the global type Ada.Exceptions.Exception_Id and are not created dynamically.

There are four predefined exceptions (there is also the obsolescent Numeric_Error which is a renamed Constraint_Error).

  1. Constraint_Error — Something is out of range, division by zero, etc.
  2. Program_Error — The control structure is violated. For example, running into the end of a function, breaking accessibility rules or calling a subprogram whose body has not been elaborated.
  3. Storage_Error — Memory has run out such as with a stack overflow.
  4. Tasking_Error — Threading errors such as failure to activate a task or being aborted mid rendezvous.

Exceptions are handled with exception handlers (the when clauses) at the end of the block and are dynamically propagated. The syntax matches that of case statements and exceptions that are propagated outside of their scope may only be caught in the others clause.

begin
   -- Something that may raise an exception
exception
   when Constraint_Error =>
      -- Handle a constraint error, if it occurs
   when others =>
      -- Handle any other exception, if one occurs
end;
Any handler that appears at the end of a package will only apply to the initialisation sequence of the package and not its contents.

Custom exceptions may be declared and raised and it is generally poor practice to raise the built-in exceptions.

My_Exception: exception;

raise My_Exception;

raise My_Exception with "Information about the exception";
-- Get the message with Ada.Exceptions.Exception_Message.
-- Without the message, its value is implementation-defined.
If you want to catch and then propagate/re-raise an exception then simply have a raise; in a handler will do so.

Note

Once an exception has been raised, control cannot be returned directly to the unit where the exception was raised. The statements following the => must terminate the unit which means that for a function there must be a return.

In addition, any goto statements should not jump into or between handlers, but they can jump to a label in a block outside of the one handling the exception or exit a loop via exit.

out and in out Parameters

If a subprogram is terminated by an exception then the value of a parameter passed in by value may not have been updated since the updating normally happens on a normal return.

Exception Information#

The package Ada.Exceptions contains the limited type Exception_Occurrence. This with the subprograms Exception_Name, Exception_Message and Exception_Information can be used to get further information about the exception that was raised. These subprograms also allows you to get information about an exception that is propagated out of its scope.

with Ada.Exceptions; use Ada.Exceptions;

begin
   -- ...
exception
   when Exn: others =>
      Put_Line(Exception_Name(Exn));
      Put_Line(Exception_Message(Exn));
      Put_Line(Exception_Information(Exn));
end;