Ada Programming (Draft)

Hodong Kim <hodong@nimfsoft.art>

Preface

This book is a tutorial on the Ada programming language, written for developers with existing programming experience.

Ada was designed with a focus on reliability, maintainability, and efficiency, making it suitable for developing high-reliability software.

All code examples in this book conform to the Clair Coding Style Guide, which is detailed in an appendix. This guide adapts common coding styles from languages like C, Java, C#, and Ruby to fit Ada’s features. Its primary goal is to provide a familiar experience for developers with a background in other languages. As a result, the Clair style intentionally diverges from some traditional Ada community conventions.


Creative Commons License This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.


Table of Contents


1. Introduction to Ada Language

Ada was the result of a deliberate engineering effort to solve a defined set of problems. Understanding this origin is fundamental to understanding its design, as nearly every feature of the language can be traced back to a formal requirement aimed at producing reliable, maintainable, and efficient software. This tutorial provides an exploration of Ada, from its foundational principles to its advanced capabilities, demonstrating its continued use in high-reliability systems.

1.1 Historical Context and Motivation: The “Software Crisis”1

In the early 1970s, the United States Department of Defense (DoD) faced what became known as a “software crisis”. A cost study revealed that the DoD was spending billions of dollars annually on software, a significant portion of which was for embedded computer systems—systems where the computer is an integral part of a larger machine, such as an aircraft, missile, or communications device. A major contributor to this cost was the large number of programming languages in use. Over 450 different languages and dialects were in use across various projects, many of which were proprietary, obsolete, or hardware-dependent.

This proliferation of languages had significant consequences. Software was rarely reusable, code could not be ported to new hardware, and maintenance—which constitutes the majority of a software system’s lifecycle cost—was difficult and expensive. The situation was not merely a matter of inefficiency; it was a strategic problem.

In response, the DoD initiated a program in 1975 to establish a single, common, high-order programming language suitable for its real-time embedded systems. To this end, it formed the High Order Language Working Group (HOLWG). The HOLWG’s first task was to define the requirements for such a language. This process was open and collaborative, involving extensive review from military departments, industrial organizations, universities, and international partners, particularly from NATO countries.

The requirements were formalized in a series of progressively refined documents, named after materials of increasing strength: Strawman, Woodenman, Tinman, Ironman, and finally, the definitive Steelman document in 1978. The Steelman requirements were comprehensive, including features that were advanced for the time. Key requirements included reliability, support for maintainability, efficiency for embedded applications, robust exception handling facilities, compile-time and run-time checking, and built-in support for parallel processing (concurrency).

After concluding that no existing language could satisfy these requirements, the DoD launched an international design competition. To ensure impartiality, the four contracting teams chosen to develop language proposals were identified only by colors:

  • Green: CII-Honeywell-Bull (led by Jean Ichbiah)
  • Red: Intermetrics (led by Benjamin Brosgol)
  • Blue: SofTech (led by John Goodenough)
  • Yellow: SRI International (led by Jay Spitzen)

Following public scrutiny and evaluation by international teams of experts, the Green and Red designs were selected for a final phase of refinement. In May 1979, the Green proposal, designed by the team at CII-Honeywell-Bull under the leadership of French computer scientist Jean Ichbiah, was chosen as the winner.

The new language was named “Ada” in honor of Augusta Ada King-Noel, Countess of Lovelace (1815-1852), the mathematician who worked with Charles Babbage and is regarded as the world’s first computer programmer. The first official language reference manual was published as a military standard, MIL-STD-1815, in 1980—the number chosen as a tribute to Ada Lovelace’s birth year. This was the result of an extensive and well-funded language design effort.

The DoD’s approach had a lasting impact. The fact that every major feature in Ada can be traced back to a specific, documented requirement in the Steelman report illustrates a process of deliberate systems engineering. This is a contrast to many languages that evolved from smaller projects. Features like strong typing, packages, and tasking are not arbitrary additions; they are engineered solutions to the problems of reliability, maintainability, and concurrency identified at the outset of the project.

Another significant historical element was the “Ada mandate.” Issued by the DoD in 1987, it stipulated that Ada was to be the single, common language for new defense computer systems, particularly in critical areas like command and control and weapons systems. This mandate was later reversed in 1997. This reversal was not a judgment on Ada’s technical capabilities but an acknowledgment of market realities. By the late 1990s, the commercial software world had invested in other languages like C++, leading to a larger ecosystem of tools and a wider pool of trained programmers. The DoD’s policy shifted to requiring the use of standard, non-proprietary languages, a category that still includes Ada but is no longer exclusive to it. This history helps explain Ada’s presence in its intended domains (defense, aerospace, and other high-integrity sectors) while also accounting for its perception as a “niche” language in the broader commercial market.

1.2 Continus Development of the Ada Standard

Ada has undergone several major revisions since its inception, each carefully managed to be upwardly compatible with previous versions. This continus development has not been a series of disparate additions but a deliberate process of enhancing the language’s core principles to meet new technological challenges.

  • Ada 83: This was the initial ANSI standard (MIL-STD-1815A). It established the foundational features of the language: packages for modularity, generics for reusable templates, tasks for concurrency, strong typing, and exception handling.

  • Ada 95: Led by S. Tucker Taft of Intermetrics, this revision was a significant enhancement that made Ada the first internationally standardized (ISO) object-oriented language. It introduced tagged types for inheritance and polymorphism, child library units for more flexible and hierarchical system structuring, and protected objects as a safer and more efficient mechanism for coordinating access to shared data in concurrent programs. These features were added to improve the design and implementation of very large, complex systems.

  • Ada 2005: This revision focused on improving interoperability and enhancing support for high-integrity real-time systems. It introduced Java-like interfaces, which provide a form of multiple inheritance, and allowed for mutual dependencies between package specifications, making it easier to interface with languages like Java. Critically, it standardized the Ravenscar Profile, a subset of Ada’s concurrency features that is simple enough to be formally analyzed, making the certification of safety-critical real-time systems more practical.

  • Ada 2012: This was another major step forward, most notably for integrating Design by Contract (DbC) directly into the language standard. Programmers can now specify preconditions, postconditions, and type invariants as part of the source code. These contracts serve as executable documentation and can be checked at runtime, formalizing the principle of provability in the language itself. Ada 2012 also introduced new features to support parallel programming on multicore processors, such as barriers and multiprocessor affinity, bringing Ada’s powerful concurrency model to modern hardware architectures.

  • Ada 2022: The most recent version of the standard continues this path of refinement, adding further enhancements to contracts, expressions, and the standard library, while maintaining the language’s stability and upward compatibility.

The continus development of Ada is a clear reflection of its design philosophy. Each revision has strengthened the language’s ability to build reliable, safe, and maintainable systems, from the introduction of object-orientation for better large-scale design in Ada 95, to the formalization of contracts for verifiable correctness in Ada 2012.

1.3 Design Goals

The following section is an unmodified excerpt from the official Ada 2022 Reference Manual. It is included to provide the authoritative explanation of the language’s core design principles.

6/5 Ada was originally designed with three overriding concerns: program reliability and maintenance, programming as a human activity, and efficiency. The 1995 revision to the language was designed to provide greater flexibility and extensibility, additional control over storage management and synchronization, and standardized packages oriented toward supporting important application areas, while at the same time retaining the original emphasis on reliability, maintainability, and efficiency. Subsequent editions, including this fourth edition, have provided further flexibility and added more standardized packages within the framework provided by the 1995 revision.

7 The need for languages that promote reliability and simplify maintenance is well established. Hence emphasis was placed on program readability over ease of writing. For example, the rules of the language require that program variables be explicitly declared and that their type be specified. Since the type of a variable is invariant, compilers can ensure that operations on variables are compatible with the properties intended for objects of the type. Furthermore, error-prone notations have been avoided, and the syntax of the language avoids the use of encoded forms in favor of more English-like constructs. Finally, the language offers support for separate compilation of program units in a way that facilitates program development and maintenance, and which provides the same degree of checking between units as within a unit.

8 Concern for the human programmer was also stressed during the design. Above all, an attempt was made to keep to a relatively small number of underlying concepts integrated in a consistent and systematic way while continuing to avoid the pitfalls of excessive involution. The design especially aims to provide language constructs that correspond intuitively to the normal expectations of users.

9 Like many other human activities, the development of programs is becoming ever more decentralized and distributed. Consequently, the ability to assemble a program from independently produced software components continues to be a central idea in the design. The concepts of packages, of private types, and of generic units are directly related to this idea, which has ramifications in many other aspects of the language. An allied concern is the maintenance of programs to match changing requirements; type extension and the hierarchical library enable a program to be modified while minimizing disturbance to existing tested and trusted components.

10/5 No language can avoid the problem of efficiency. Languages that require over-elaborate compilers, or that lead to the inefficient use of storage or execution time, force these inefficiencies on all machines and on all programs. Every construct of the language was examined in the light of present implementation techniques. Any proposed construct whose implementation was unclear or that required excessive machine resources was rejected. Parallel constructs were introduced to simplify making safe and efficient use of modern multicore architectures.

Source: ISO/IEC 8652:2023(E), Ada Reference Manual, Introduction, paragraphs 6-10.

1.4 Main features

An Ada program is composed of one or more program units. These units are designed to be largely independent software components, which facilitates the design, implementation, and testing of complex programs. There are several types of program units, each serving a specific purpose:

  • Subprograms: The basic units for defining algorithms. They can be either procedures, which perform a series of actions, or functions, which compute and return a value. Both can use parameters to exchange information.
  • Packages: The fundamental units for organizing related entities, such as type declarations and their associated operations. Packages have a specification and a body. The specification contains information visible to other units, while the body contains the implementation details, which are hidden from external access. This separation of concerns allows for controlled and structured access to the package’s contents. Packages can also be arranged in hierarchical parent-and-child relationships, providing fine-grained control over visibility.
  • Task Units: Used to define tasks that can execute concurrently. This allows a program to perform multiple sequences of actions simultaneously, which can be implemented on a single processor with interleaved execution or across multiple processors.
  • Protected Units: Provide a mechanism for the coordinated sharing of data between concurrent tasks. They automatically ensure mutual exclusion, preventing simultaneous access to shared data. More complex sharing protocols can also be defined.

Most program units can be compiled separately, which allows for the development of a program as a collection of modular components. Additionally, the language provides facilities for creating libraries of general-purpose program units. These libraries can be structured hierarchically, enabling the logical decomposition of a system into manageable components.

The body of a program unit is typically divided into two parts: a declarative part and a sequence of statements.

  • The declarative part defines the local entities, such as types, constants, variables, and nested program units, that are used within the unit.
  • The sequence of statements specifies the actions to be performed during execution. These can include assignment statements to change variable values, control structures like if and case statements for conditional execution, and loop statements for iteration.

Ada also includes specialized statements for concurrent programming, such as delay statements to pause a task and various forms of the select statement to manage communication between tasks.

Data Types and Error Handling

Every object in Ada is associated with a type, which defines a set of possible values and applicable operations. Types are broadly categorized into elementary types and composite types.

  • Elementary Types include enumeration types, which define an ordered set of distinct literals (e.g., Boolean, Character); and numeric types, which support exact (integer) or approximate (fixed and floating-point) computations.
  • Composite Types allow for the creation of structured objects with multiple components. Examples include array types, which have indexed components of the same type, and record types, which have named components of potentially different types. Task and protected types are also considered composite types.

The concept of a type is further refined by subtypes, which constrain the set of allowed values for a given type. This allows a user to define a limited range for a scalar type or an array with a restricted set of index values.

Ada also supports object-oriented programming capabilities. A new type can be created by derivation from an existing type. A tagged type and its derivatives form a derivation class, which allows for class-wide operations and type extension. This enables dynamic dispatching, where the specific operation to be executed is determined at run time based on the type of the operand.

To handle runtime errors, Ada provides an exception handling mechanism. When an error situation occurs, such as an arithmetic overflow or an invalid array index, an exception can be raised. Program units can include exception handlers to specify the actions to be taken when such an error arises, ensuring the program can respond predictably.

Other Features

Ada provides various other features to support robust and efficient software development:

  • Aspect clauses: Allow a developer to specify low-level, machine-dependent properties, such as the memory layout of data, and more abstract properties, such as pre- and postconditions for subprograms.
  • Standard Library: The language includes a rich predefined library of packages that offer functionalities for input-output, string manipulation, data containers, and mathematical functions.
  • Generic Program Units: Ada supports the parameterization of program units through generic units. This allows the creation of general-purpose algorithms and data structures that can operate on different types, promoting code reuse and flexibility.

2. Development Environment and First Steps

Before writing Ada code, it is essential to set up a professional development environment. This section covers the GNAT toolchain, the primary compiler for Ada, and common Integrated Development Environments (IDEs). It then guides the reader through compiling and running a foundational “Hello, World!” program, establishing best practices from the outset.

2.1 The GNAT Toolchain

The GNU NYU Ada Translator (GNAT) is the most widely used compiler and toolchain for Ada. It is free, open-source, and part of the GNU Compiler Collection (GCC). The GNAT project was initiated in 1992 through a contract from the United States Air Force to New York University (NYU). The goal was to create a high-quality, free Ada compiler to support the upcoming Ada 9X standardization process (which became Ada 95). The copyright for the resulting compiler was assigned to the Free Software Foundation, ensuring its continued availability.

The original authors of GNAT later founded AdaCore to provide commercial-grade support, tools, and continued development for the GNAT technology. AdaCore offers both a free, community edition of GNAT and a commercially supported version, GNAT Pro.

The core components of the GNAT toolchain include:

  • GNAT Compiler (gcc): The compiler that translates Ada source code (.ads, .adb files) into object files (.o files).

  • GNAT Binder (gnatbind): A tool that processes the Ada Library Information (.ali) files generated by the compiler to check for consistency and resolve dependencies across compilation units.

  • GNAT Linker (gnatlink): The final tool that links the object files with the Ada runtime library to create an executable file.

  • GNAT Make (gnatmake): A utility that automates the compile-bind-link process. It analyzes the dependencies in Ada source files (via with clauses) and automatically compiles only the necessary files in the correct order.

  • GNAT Project Manager (gprbuild): A more advanced build tool that uses project files (.gpr) to manage complex projects with multiple source directories, different languages, and various compiler options.

Installation instructions for the GNAT Community edition can be found on the AdaCore website. On Linux systems, GNAT can often be installed directly through the distribution’s package manager (e.g., sudo apt install gnat on Debian-based systems or sudo dnf install gcc-gnat on Fedora-based systems).

2.2 Integrated Development Environments (IDEs)

While Ada code can be written in any text editor, using an IDE with language-aware features significantly enhances productivity. Two primary options are GNAT Studio and Visual Studio Code.

GNAT Studio

GNAT Studio is the dedicated, lightweight IDE for Ada and SPARK developed by AdaCore. It provides a tightly integrated environment for editing, building, debugging, and formally verifying Ada code. Key features include:

  • Project Management: A wizard for creating and managing GNAT project files (.gpr).

  • Source Code Editor: Provides syntax highlighting, code completion, and source navigation.

  • Build Integration: One-click actions to build, clean, and run the project.

  • Ada-Aware Debugger: A graphical front-end for GDB that understands Ada-specific constructs, such as task states, protected objects, and complex record types.

A typical workflow in GNAT Studio involves creating a new project, adding source files, using the ‘Build -> Project -> Build’ All menu to compile, and then using the ‘Debug -> Run’ menu to execute or debug the application.

Visual Studio Code with AdaCore Extension

Visual Studio Code (VS Code) is a popular, general-purpose code editor that can be configured for Ada development using an official extension from AdaCore. The AdaCore.ada extension provides rich language support by integrating the Ada Language Server.

To set up VS Code for Ada development:

  1. Install Visual Studio Code.
  2. Install the GNAT toolchain and ensure it is in the system’s PATH.
  3. Install the ‘Ada & SPARK’ extension from the VS Code Marketplace by searching for AdaCore.ada.

The extension provides the following features:

  • Syntax highlighting, code navigation, and autocompletion.

  • Integration with gprbuild for building projects via VS Code tasks. The default build task can be run with the shortcut Ctrl + Shift + B.

  • Debugging support, allowing users to set breakpoints, step through code, and inspect variables.

A typical workflow in VS Code involves creating a workspace folder, adding a GNAT project file (.gpr), writing source code, and using the built-in tasks to compile and debug the program.

2.3 A Foundational Program: “Hello, World!”

The traditional first program in any new language is “Hello, World!”. In Ada, even this simple program introduces several core concepts of the language’s structure and philosophy. The following example is written in accordance with the Clair Coding Style Guide, which will be used for all code in this tutorial.

Create a file named hello.adb. The convention of matching the filename to the main procedure name is important for avoiding compiler warnings.

with Ada.Text_IO;

procedure hello is
begin
  Ada.Text_IO.put_line ("Hello, World!");
end hello;

To compile and run this program from the command line, use gnatmake:

$ gnatmake hello.adb
$./hello
Hello, World!

The gnatmake command automates the three-step process of compiling (gcc -c hello.adb), binding (gnatbind hello.ali), and linking (gnatlink hello.ali) to produce the final executable file, hello.

Let us deconstruct this simple program:

  • with Ada.Text_IO;: This is a with clause. It declares a dependency on the library package Ada.Text_IO. This makes the public declarations within that package, such as the put_line procedure, available to the hello procedure. This is the fundamental mechanism for modularity in Ada. From the very first program, Ada enforces a structured, modular approach where dependencies must be explicitly declared. This contrasts with languages that might have global, implicitly available I/O functions.

  • procedure hello is ... end hello;: This defines the main program unit. In Ada, the entry point of a program is a parameterless procedure. The structure consists of:

    • The procedure keyword and the name (hello).

    • The is keyword, which separates the procedure’s header from its declarative part. The space between is and begin is where local variables, types, or nested subprograms would be declared.

    • The begin keyword, which marks the start of the executable statements.

    • The end hello; statement, which marks the end of the procedure. The explicit repetition of the procedure name (hello) is a stylistic convention that enhances readability and helps the compiler check for correctly matched blocks.

  • Ada.Text_IO.put_line ("Hello, World!");: This is a subprogram call.

    • Ada.Text_IO.put_line: This uses dot notation to call the put_line procedure, which is located inside the Ada.Text_IO package. This explicit qualification is a best practice that avoids namespace pollution and makes it clear where the subprogram is defined.

    • ` (…)`: A space is used between the subprogram name and the opening parenthesis, as required by the Clair style guide, to visually distinguish subprogram calls from other constructs.

    • "Hello, World!": This is a string literal, which is the argument passed to the put_line procedure.

    • ;: The semicolon is a statement terminator in Ada.

The structure of this first program reveals a key aspect of Ada’s design: the toolchain is intelligent and language-aware. When gnatmake is invoked, it does not simply compile the given file. It parses the Ada source, reads the with Ada.Text_IO; clause, and understands that it must link against the pre-compiled Ada.Text_IO library unit. This tight integration between the language’s modularity features and its build tools is what makes managing large, multi-file systems tractable, fulfilling one of the original design goals of the language.

3. The Ada Type System

Ada’s type system is a central feature and the primary instrument for achieving the language’s goals of reliability and correctness. It is a mechanism for modeling the problem domain, embedding application-specific rules into the code, and enabling the compiler to detect a wide range of logical errors before a program is run.

3.1 The Principle of Strong Typing

Ada is a strongly typed language. This is a design choice intended to increase compile-time error detection. The system is founded on several key rules:

  • Name Equivalence: Two variables are of the same type only if they are declared with the same type name. It is not sufficient for them to have the same underlying structure or representation. For example, consider two integer types:

      type Apples  is range 0 .. 1_000;
      type Oranges is range 0 .. 1_000;
    
      num_apples  : Apples;
      num_oranges : Oranges;
    
  • No Implicit Conversions: The compiler will not perform implicit type conversions between different types, particularly numeric types. If a programmer wishes to assign a value of type Apples to a variable of type Oranges, they must do so explicitly: num_apples := Apples (Num_Oranges);. This forces the programmer to acknowledge the conversion, making the intent clear and preventing subtle errors. A classic example of the danger of implicit conversion is integer division. In C++ or Java, the expression float result = 5 / 2; yields 2.0, because an integer division is performed first, and the result (2) is then implicitly converted to a float. In Ada, the equivalent operation would require explicit conversions, result := Float (5) / Float (2);, which correctly performs a floating-point division and yields 2.5.

    This strictness allows the type system to serve as a domain modeling tool. By creating distinct types for different physical quantities (e.g., Meters, Kilograms, Seconds), a programmer can use the compiler to ensure that these quantities are never incorrectly combined, thereby preventing a class of logical and physical modeling errors.

3.2 Scalar Types

Scalar types represent single values, such as numbers or characters. Ada provides a set of facilities for defining custom scalar types.

Integer Types

While Ada provides a predefined Integer type, it recommends the creation of problem-specific integer types with constrained ranges.

type Day_Of_Month is range 1 .. 31;
type Engine_RPM   is range 0 .. 7_000;

Declaring a variable of type Day_Of_Month not only restricts its value to the specified range but also makes it incompatible with other integer types like Engine_RPM. Any attempt to assign a value outside the defined range, if not caught at compile time, will raise the predefined Constraint_Error exception at runtime.

The standard library also provides two useful predefined subtypes of Integer:

  • subtype Natural is Integer range 0 .. Integer'last;
  • subtype Positive is Integer range 1 .. Integer'last;

Enumeration Types

Enumeration types are those whose values are specified as an ordered list of identifiers. They are useful for modeling states, modes, or any set of named values.

type Traffic_Light_Color is (Red, Amber, Green);
type Day_Of_Week is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);

The values are ordered, so relational operators like < and > are defined (e.g., Red < Amber is true).

Real Types (Floating-Point and Fixed-Point)

For computations involving real numbers, Ada provides two kinds of types:

  • Floating-Point Types: Used for approximate computations where relative error is important. The programmer specifies the required number of decimal digits of precision.

      type Percentage is digits 7 range 0.0 .. 100.0;
      type Mass is digits 15; -- A floating point type with no range constraint
    
  • Fixed-Point Types: Used for approximate computations where absolute error is critical, such as financial calculations. The programmer specifies the delta, which is the absolute error bound.

      type Dollars is delta 0.01 range 0.0 .. 1_000_000.00;
    

    This guarantees that values of type Dollars are represented with an error no greater than 0.01.

Subtypes

A subtype does not create a new type. Instead, it provides an optional constraint on an existing base type. A subtype is compatible with its base type and other subtypes of the same base type.

subtype Work_Day is Day_Of_Week range Monday .. Friday;

today    : Day_Of_Week;
pay_day  : Work_Day;
...
today := pay_day; -- Always legal
-- Legal, but raises Constraint_Error at runtime if Today is Saturday or Sunday
pay_day := today;

The choice between creating a new type and a subtype is a fundamental design decision. Creating a new type enforces strict separation and prevents accidental mixing of concepts (e.g., Celsius and Fahrenheit). Creating a subtype allows for compatibility while enforcing constraints, which is useful for refining a single concept (e.g., Work_Day is a subset of Day_Of_Week).

3.3 Composite Types

Composite types represent collections of other types.

Arrays

Arrays are collections of components all of the same type, indexed by a discrete type (an integer or enumeration type). Ada makes a distinction between constrained arrays, whose bounds are fixed at compile time, and unconstrained arrays, whose bounds can be determined at runtime.

  • Constrained Array:

      type Vector is array (1 .. 10) of Float;
      my_vector : Vector; -- Bounds are fixed to 1 .. 10
    
  • Unconstrained Array: The bounds are indicated by <> (a “box”).

      type Matrix is array (Integer range <>, Integer range <>) of Float;
    
      -- Bounds are specified when an object is declared
      m1 : Matrix (1 .. 10, 1 .. 20);
      m2 : Matrix (0 .. 4,  0 .. 4);
    

The predefined String type is an unconstrained array: type String is array (Positive range <>) of Character;.

Records

Records are collections of named components, which can be of different types. They are analogous to structs in C.

type Date is record
  year  : Integer range 1900 .. 2100;
  month : Integer range 1 .. 12;
  day   : Day_Of_Month; -- Uses the type defined earlier
end record;

type Person is record
  name      : String (1 .. 30);
  birthdate : Date;
end record;

3.4 Access Types (“Pointers”)

Access types in Ada are the equivalent of pointers or references in other languages, but they are designed with an emphasis on safety.

  • Strongly Typed: An access type is “tied” to the specific type it can designate. There are no untyped (void*) pointers, which prevents a common source of type errors.

  • No Pointer Arithmetic: It is illegal to perform arithmetic operations on access values. This restriction eliminates a class of memory corruption bugs and security vulnerabilities.

  • Declaration and Use: An access type is declared with the access keyword. The new allocator is used to create an object on the heap and returns an access value designating that object. The .all suffix is used to dereference the access value and access the designated object.

procedure main is
  -- Declare an access type that can only point to Integer objects
  type Integer_Access is access Integer;

   -- Declare a procedure for deallocation, instantiating the generic
   -- package with our specific object and access types.
   procedure free is new Ada.Unchecked_Deallocation (
      object => Integer,
      name   => Integer_Access
   );

  -- Declare an access variable, initialized to null by default
  p : Integer_Access;

begin
  -- Allocate a new Integer object on the heap with the value 42,
  -- and make p point to it.
  p := new Integer'(42);

  -- Access the value of the designated object using.all
  Ada.Text_IO.put_line (Integer'image (p.all)); -- Prints " 42"

  p.all := p.all + 1; -- The designated object is now 43

  free (p);
end main;

By enforcing these rules, Ada’s access types provide the power of dynamic data structures without the risks associated with traditional pointers.

4. Control Flow and Statements

Ada’s control flow structures are designed with an emphasis on clarity, explicitness, and the prevention of common structural programming errors. The syntax is more verbose than that of C-family languages, a choice made to eliminate ambiguity and improve long-term maintainability.

4.1 Conditional Statements

if Statements

Ada employs a fully blocked if ... then ... elsif ... else ... end if structure. The condition following the

if or elsif keyword must evaluate to the predefined Boolean type (True or False).

A key feature of this structure is the mandatory end if; terminator. This syntactic requirement eliminates the “dangling else” ambiguity that can occur in languages with optional block delimiters like C or C++. In Ada, it is syntactically impossible to create a situation where it is unclear which if an else clause belongs to. This is an example of Ada’s design philosophy: using the language syntax itself to prevent a class of bugs.

-- Example of a complete if-elsif-else structure
if temperature > HIGH_TEMP_THRESHOLD then
  activate_emergency_coolant;
elsif temperature > NORMAL_TEMP_LIMIT then
  increase_fan_speed;
else
  -- No action needed, temperature is within normal operating range.
  null;
end if;

case Statements

The case statement provides a clear and safe mechanism for multi-way branching based on the value of a single expression of a discrete type (i.e., any integer or enumeration type).

type Day_Of_Week is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);
today : Day_Of_Week := Wednesday;
...
case today is
  when Monday.. Friday =>
    schedule_work_meeting;
  when Saturday | Sunday =>
    plan_weekend_activity;
end case;

The case statement has two safety features:

  1. No “Fall-through”: Execution does not fall from one case to the next. Each when branch is independent, eliminating a common source of bugs in C-family languages where a forgotten break statement causes unintended execution.

  2. Compile-Time Coverage Checking: The Ada compiler verifies that every possible value of the case expression’s type is covered by a when clause. If any value is omitted, the compiler will issue an error unless a when others clause is provided to handle all remaining possibilities. This feature assists the programmer during maintenance. If a programmer modifies an enumeration type by adding a new value (e.g., adding a new state to a state machine), the compiler will automatically flag every case statement in the codebase that operates on that type as being incomplete. This automates an aspect of maintenance, ensuring that no state is left unhandled.

-- This case statement would cause a compile-time error if Day_Of_Week
-- included other values not covered, unless a 'when others' is added.
case Today is
  when Monday =>
 ...
  when Tuesday =>
 ...
  -- etc. for all 7 days
end case;

-- A valid alternative using 'when others'
case User_Input is
  when 'Y' | 'y' =>
    confirm_action;
  when 'N' | 'n' =>
    cancel_action;
  when others =>
    report_invalid_input;
end case;

4.2 Iterative Statements (Loops)

Iterative statements, commonly known as loops, provide a mechanism for repeatedly executing a sequence of statements. Ada offers several forms of loops, each suited for different iteration requirements, from simple repetition to complex, parallel processing. These constructs are fundamental for controlling the flow of execution in a program.

4.2.1 Basic Loop

The most fundamental loop structure is the basic loop. It defines a sequence of statements that repeats indefinitely. To prevent an infinite loop, an exit statement is required to terminate the iteration and transfer control to the statement following the loop.

Syntax:

loop
  -- sequence_of_statements
  exit when condition;
  -- more_statements
end loop;

The exit when statement evaluates a condition on each iteration. If the condition is True, the loop terminates.

Example: The following code reads characters until an asterisk * is encountered.

with Ada.Text_IO;

procedure read_until_stop is
  use Ada.Text_IO;
  current_character : Character;
begin
  loop
    get (current_character);
    exit when current_character = '*';
  end loop;
  new_line;
  put_line ("Loop terminated.");
end read_until_stop;

4.2.2 while Loop

A while loop executes its body as long as a specified condition remains True. The condition is checked before the start of each iteration. If the condition is initially False, the loop body will not execute at all.

Syntax:

while condition loop
  -- sequence_of_statements
end loop;

Example: This loop processes bids as long as the bid price is below a certain cutoff.

-- Assuming bid, price, record_bid, cut_off are declared elsewhere.
while bid(n).price < cut_off.price loop
  record_bid (bid(n).price);
  n := n + 1;
end loop;

4.2.3 for Loop

The for loop is a fundamental construct for definite iteration, meaning it is designed to execute a sequence of statements a specific number of times. This number is determined by a discrete range, which is evaluated once before the loop begins.

A discrete type in Ada is one whose values have a clear, distinct separation between them. This category includes:

  • Integer types (e.g., Integer, Natural, Positive)
  • Enumeration types, including the predefined Boolean and Character types.

Because the values are countable and ordered, the loop can iterate through each one sequentially. This deterministic nature—knowing the exact number of iterations in advance—is a key characteristic of the for loop. It cannot be used with non-discrete types like Float or String.

The loop introduces a loop parameter, an identifier that takes on the value of each successive element in the discrete range during each iteration. This loop parameter has three important properties:

  1. It is implicitly declared by the loop itself; no separate declaration is needed.
  2. Its scope is confined to the loop body, from the loop keyword to the end loop keywords. It is not visible outside the loop.
  3. It is a constant within the loop. Its value is automatically updated by the loop mechanism at the start of each iteration and cannot be modified by the code inside the loop.

Iteration over Discrete Ranges

The most common form iterates over a range of values specified with the .. operator. The loop can proceed in ascending order (the default) or in descending order using the reverse keyword.

Example (Integer Range):

with Ada.Text_IO;

procedure integer_loop_example is
begin
  -- Iterates from 1 up to 10
  for i in 1 .. 10 loop
    Ada.Text_IO.put_line ("Ascending: " & Integer'image (i));
  end loop;

  Ada.Text_IO.new_line;

  -- Iterates from 10 down to 1
  for i in reverse 1 .. 10 loop
    Ada.Text_IO.put_line ("Descending: " & Integer'image (i));
  end loop;
end integer_loop_example;

Example (Enumeration Range): This example demonstrates iteration over a custom enumeration type.

with Ada.Text_IO;

procedure enum_loop_example is
  type Day_Of_Week is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);
begin
  for day in Day_Of_Week'range loop  -- 'range provides the full range
    Ada.Text_IO.put (Day_Of_Week'image (day) & " ");
  end loop;
  Ada.Text_IO.new_line;

  -- Iterating over a subrange of the enumeration
  for day in Wednesday .. Friday loop
    Ada.Text_IO.put_line ("Workday: " & Day_Of_Week'image (day));
  end loop;
end enum_loop_example;

The Principle of Single Range Evaluation

A defining characteristic of the Ada for loop is that its discrete range is evaluated only once. This means the range is fully calculated and its bounds are fixed before the first iteration begins. This single evaluation establishes an immutable sequence of values for the loop parameter, and consequently, the number of iterations is determined before the loop body is ever executed.

This principle directly governs the behavior of loops with a null range. A range is considered null if its lower bound is greater than its upper bound (e.g., 5 .. 1). When the range is null, the loop completes immediately, performing zero iterations. The sequence of statements within the loop body is never executed.

The reverse keyword affects only the direction of iteration, not the validity of the range itself, as it does not swap the range’s bounds. For this reason, the loop for j in reverse 1 .. 0 loop will not execute. The range 1 .. 0 is determined to be null before the reverse keyword is considered for the iteration order. This provides a safe and predictable outcome.

Example: Null Range Behavior

The following example demonstrates that a loop with a null range does not execute its body, even if the reverse keyword is used.

with Ada.Text_IO;

procedure null_range_example is
  use Ada.Text_IO;
  loop_1_executed : Boolean := False;
  loop_2_executed : Boolean := False;
begin
  -- This loop will not execute because 5 is greater than 1.
  for i in 5 .. 1 loop
    loop_1_executed := True;
  end loop;

  if not loop_1_executed then
    put_line ("Loop with range 5 .. 1 did not execute.");
  end if;

  -- Using 'reverse' does not fix a null range.
  -- This loop will also not execute because 1 is greater than 0.
  for j in reverse 1 .. 0 loop
    loop_2_executed := True;
  end loop;

  if not loop_2_executed then
    put_line ("Loop with range 'reverse 1 .. 0' did not execute.");
  end if;
end null_range_example;

Output:

Loop with range 5 .. 1 did not execute.
Loop with range 'reverse 1 .. 0' did not execute.

This design ensures that for loops are predictable and deterministic. Unlike the for loop in C, where the termination condition is re-evaluated before each iteration and can be modified within the loop, an Ada for loop’s iteration count is unaffected by changes made to the range’s bounds inside its body.

Example: Immutable Iteration Count

The following code shows that even if a variable used to define the loop’s range is modified within the loop, the original number of iterations does not change.

with Ada.Text_IO;

procedure immutable_bounds_example is
  use Ada.Text_IO;
  upper_bound : Integer := 3;
begin
  put_line ("Loop will start with range 1 .. " & Integer'image (upper_bound));
  new_line;

  -- The range '1 .. upper_bound' is evaluated here as '1 .. 3'.
  -- The loop is now fixed to execute exactly 3 times.
  for i in 1 .. upper_bound loop
    put_line ("Start of iteration " & Integer'image (i) &
              ": upper_bound is " & Integer'image (upper_bound));

    if i = 2 then
      upper_bound := 10; -- This change has no effect on the loop's duration.
      put_line (" -> upper_bound changed to 10 inside the loop.");
    end if;
  end loop;

  new_line;
  put_line ("Loop finished. It executed 3 times as determined initially.");
  put_line ("Final value of upper_bound: " & Integer'image (upper_bound));
end immutable_bounds_example;

Output:

Loop will start with range 1 ..  3

Start of iteration  1: upper_bound is  3
Start of iteration  2: upper_bound is  3
 -> upper_bound changed to 10 inside the loop.
Start of iteration  3: upper_bound is  10

Loop finished. It executed 3 times as determined initially.
Final value of upper_bound:  10

This behavior prevents a common class of errors, such as accidental infinite loops, thereby enhancing program safety and reliability. It also improves code readability, as a programmer can determine the exact number of iterations simply by reading the loop’s declaration.

Array and Container Iteration

The for loop can also iterate over the elements of an array or an iterable container.

  • Iteration over Array Indices: The `` attribute provides the bounds for an array’s index, which is useful for accessing each element by its index.

    buffer : String (1 .. 10);
    -- ... (buffer is initialized)
    for i in buffer'range loop
      if buffer (i) /= ' ' then
        Ada.Text_IO.put (buffer (i));
      end if;
    end loop;
    
  • Iteration over Array Elements (Component Iteration): Ada allows direct iteration over the components (elements) of an array. The loop parameter in this case refers directly to an element. If the array is not a constant, the loop parameter is a variable, allowing for direct modification of the array’s elements.

    my_array : array (1 .. 5) of Integer := [10, 20, 30, 40, 50];
    begin
      for element of my_array loop
        element := element * 2; -- Modifies the element in my_array
      end loop;
    end;
    

Iterator Filter

An iterator_filter with a when clause can be added to a for loop to conditionally execute the loop body only for values that satisfy a certain condition.

Example: This loop processes only the odd numbers in the range.

for i in 1 .. 10 when i mod 2 /= 0 loop
  Ada.Text_IO.put_line ("Processing odd number: " & Integer'image (i));
end loop;

4.2.4 Loop Naming

Loops can be given a name. This practice improves readability and is essential for exiting a specific loop from within a nested loop structure. The name must appear at both the beginning and the end of the loop.

Syntax:

Loop_Name:
  loop
    -- ...
    exit Loop_Name when condition;
    -- ...
  end loop Loop_Name;

Example:

Outer_Loop:
  for i in 1 .. 10 loop
    Inner_Loop:
      for j in 1 .. 10 loop
        -- ...
        exit Outer_Loop when i * j > 50;
      end loop Inner_Loop;
  end loop Outer_Loop;

4.2.5 Parallel Loops (Ada 2022)

Ada 2022 introduces parallel loops, which allow the iterations of a for loop to be executed concurrently. This feature is designed to improve performance on multi-core processors for computationally intensive tasks by distributing the loop’s workload across multiple logical threads of control.

Implementation Note: The parallel loop feature is a addition to the Ada 2022 standard. As of GCC 15, the GNAT compiler has not yet implemented support for the parallel keyword. Therefore, the following syntax and examples are based on the Ada 2022 specification and may not be compilable with current GNAT versions.

A parallel loop is indicated by the parallel keyword, which precedes the standard for loop syntax. The runtime system partitions the loop’s iterations into one or more “chunks,” with each chunk being processed by a separate logical thread. It is important to note that the reverse keyword is not permitted in any form of parallel loop.

Ada 2022 defines three primary forms of parallel iteration:

1. Parallel for Loops over Discrete Ranges

This is the most direct form of a parallel loop, where the iterations over a discrete range (e.g., an integer or enumeration range) are parallelized.

Syntax:

parallel for loop_parameter in discrete_subtype_definition loop
  -- sequence_of_statements
end loop;

Example:

-- Performs independent calculations for each index in parallel.
parallel for i in 1 .. 100 loop
  process_data (i);
end loop;

2. Parallel Iterators for Arrays and Containers

This form allows for parallel iteration over the elements of an array or any container type that provides a parallel iterator interface (specifically, a type descended from Ada.Iterator_Interfaces.Parallel_Iterator).

Syntax:

parallel for element of Iterable_Container loop
  -- sequence_of_statements
end loop;

Example (Parallel Component Iteration): This example doubles each element of a two-dimensional array in parallel.

-- Board is a two-dimensional array of Float.
parallel for element of Board loop
  element := element * 2.0;
end loop;

3. Parallel Procedural Iterators

This advanced form uses a procedure to control the iteration. The loop is parallelized if the designated procedure has the Parallel_Iterator => True aspect. This indicates that the procedure itself is designed to safely invoke the loop body from multiple threads.

Syntax:

parallel for (parameters) of Iterator_Procedure loop
  -- sequence_of_statements
end loop;

Chunk Specification

Programmers can influence how the workload is divided by providing a chunk_specification. This allows for fine-tuning the granularity of the parallel execution. There are two ways to specify chunks:

  1. By Maximum Number: An integer expression defines the maximum number of chunks.
  2. By Chunk Subtype: A discrete subtype defines the set of chunks. This form also implicitly declares a chunk parameter that can be used inside the loop to identify the specific chunk, which is useful for partitioning results.

Example (Chunk Specification with a Subtype): This loop uses a maximum of 8 chunks to perform partial calculations. The Chunk parameter, of type Chunk_Number, is available inside the loop to distinguish which thread is operating on which part of the data.

declare
  subtype Chunk_Number is Natural range 1 .. 8;
  partial_sum : array (Chunk_Number) of Natural := (others => 0);
  grid        : array (1 .. 1_000) of Boolean;
  -- ... grid is initialized
begin
  parallel (chunk in Chunk_Number)
  for i in grid'range loop
    if grid(i) then
      -- This operation must be thread-safe. The example uses '@' for
      -- brevity, but a real implementation would require an atomic
      -- operation or a protected object for thread-safe updates.
      partial_sum(chunk) := @ + 1;
    end if;
  end loop;
  -- ... Aggregate final results from partial_sum.
end;

4.2.6 Advanced Iterators: A Brief Overview

The preceding sections have covered the fundamental and most common loop constructs in Ada. However, the language’s iteration capabilities extend beyond these forms, offering expressive, type-safe, and extensible patterns for traversing data structures. These advanced features allow programmers to define custom iteration behaviors for their own types and to use advanced iteration control mechanisms.

This section provides a high-level introduction to these advanced concepts. A comprehensive treatment, including detailed implementation examples, is presented in Chapter 6, “Advanced Iteration and Container Design.”

Generalized Iterators

While the for element of container syntax provides a direct way to iterate over the elements of an array or container, Ada also supports a more explicit form of iteration using a generalized iterator. This form uses the in keyword with an iterator object, rather than the of keyword with a container object.

This pattern is useful when a type offers multiple ways to iterate (e.g., in forward order, reverse order, or by keys versus values) or when the iteration logic is too complex to be a container’s default behavior.

Conceptual Syntax:

-- My_Container.Iterate returns an iterator object.
for item in My_Container.Iterate (Mode => By_Value) loop
  -- process item
  null;
end loop;

In this structure, My_Container.Iterate is a function that returns an iterator object, which the for ... in loop then uses to traverse the sequence.

Procedural Iterators

A procedural iterator is a distinct iteration pattern that inverts the control of the loop. Instead of the loop retrieving values from a container, the loop provides its body—as a procedure parameter—to an iterating procedure that controls the execution.

The loop body is implicitly converted to an access-to-procedure value and passed to the designated procedure. This is a versatile pattern that is used by parts of the Ada standard library, such as for iterating over environment variables.

Conceptual Syntax:

-- Iterate is a procedure that accepts an access to a procedure.
for (name, value) of Ada.Environment_Variables.Iterate loop
  put_line (name & "=" & value);
end loop;

This is syntactically equivalent to declaring a local procedure for the loop body and passing its 'Access attribute to the Iterate procedure. This level of abstraction will be detailed in Chapter 6.

The Iterable Type Mechanism

The ability to use the direct for...of... syntax on custom container types is not built-in; it is enabled by a set of language features, primarily aspects, defined in Ada.Iterator_Interfaces. By specifying aspects such as Default_Iterator and Iterator_Element for a custom type, a programmer can integrate the type directly into Ada’s iteration framework. This mechanism is essential for creating new, reusable, and efficient iterable containers.

These advanced capabilities demonstrate the structured design of Ada’s iteration model, providing layers of abstraction that range from basic loops to fully customizable, type-safe traversal patterns. For a complete guide to using and implementing these features, please refer to Chapter 6.

4.3 Blocks and Scope

A declare ... begin ... end block can be inserted anywhere a statement is allowed. This construct is useful for creating a local scope to declare temporary variables that are needed only for a small section of code, thereby improving locality and reducing the risk of accidental modification of variables from an outer scope. Blocks are also the primary mechanism for defining localized exception handlers, which are covered in detail in Section 7.

-- Outer scope
...
declare
  -- Local variables visible only inside this block
  temp     : Float;
  swap_var : Integer;
begin
  -- Operations using temp and swap_var
...
end;
-- temp and swap_var no longer exist here
...

5. Structuring with Subprograms and Packages

As programs grow in complexity, the mechanisms for abstraction become paramount. Ada provides a system for managing this complexity through subprograms, packages, and generics. These features work in concert to enable the construction of software that is modular, reusable, and maintainable over long lifecycles.

5.1 Subprograms: Procedures and Functions

Subprograms are the fundamental units of executable code in Ada. They provide algorithmic abstraction by encapsulating a sequence of operations into a single, callable entity. The language makes a clear distinction between two forms of subprograms :

  • Procedures: A procedure performs an action and is invoked as a standalone statement. It does not return a value.

  • Functions: A function computes and returns a value and is therefore always part of an expression. The return value of a function cannot be ignored; it must be assigned to a variable or used in some other expression.

Every subprogram consists of a specification (its public interface) and a body (its implementation), which can be compiled separately.

Parameter Modes

A crucial aspect of subprogram design in Ada is the explicit definition of parameter modes, which specify the intended direction of data flow for each parameter. This serves as a contract between the subprogram and its caller.

  • in: The parameter’s value is passed into the subprogram, which can only read it. The formal parameter behaves as a constant inside the subprogram. This is the default mode if none is specified.

  • in out: The parameter’s value can be both read and modified by the subprogram. The change is reflected in the actual parameter provided by the caller.

  • out: The subprogram is expected to assign a value to the parameter. The initial value of the actual parameter is irrelevant, and inside the subprogram, the formal parameter is considered uninitialized until it is assigned a value.

procedure compute_roots (
  a, b, c : in  Float;
  root_1  : out Float;
  root_2  : out Float;
  is_valid: out Boolean) is
...
end compute_roots;

Parameter Association

When calling a subprogram, parameters can be passed using two notations:

  1. Positional Association: Arguments are matched to formal parameters based on their order in the list.

  2. Named Association: Arguments are explicitly matched to formal parameters by name using the => symbol.

-- Positional call
compute_roots (1.0, -5.0, 6.0, r1, r2, v);

-- Named call (order does not matter)
compute_roots (
  a        => 1.0,
  b        => -5.0,
  c        => 6.0,
  root_1   => r1,
  root_2   => r2,
  is_valid => v);

Named association is recommended for subprograms with multiple parameters, as it improves code clarity and reduces the risk of passing arguments in the wrong order.

Expression Functions

Introduced in Ada 2012, expression functions provide a concise syntax for defining simple functions whose body consists of a single return expression.

function square (x : Integer) return Integer is (x * x);

5.2 Packages for Modularity

The package is Ada’s principal mechanism for modularity, encapsulation, and information hiding. It allows a programmer to group a collection of logically related entities—such as types, constants, variables, and subprograms—into a single, named module with a well-defined interface.

A package is always split into two distinct parts, which are stored in separate files and compiled independently :

  • Package Specification (.ads file): This defines the public interface of the package. It contains all the declarations that are intended to be visible and usable by other parts of the program (the “clients” of the package). This is the contract the package offers to the outside world.

  • Package Body (.adb file): This contains the implementation of the entities declared in the specification. This includes the complete bodies of all subprograms, as well as any internal data structures, helper subprograms, or constants that are necessary for the implementation but should be hidden from clients.

This strict separation of interface and implementation is a key principle of large-scale software engineering in Ada. It allows a team to define and agree upon the package specifications first. Once the specifications are compiled, different developers can work on implementing the package bodies in parallel, confident that the compiler will enforce the agreed-upon interfaces. Furthermore, the implementation inside a package body can be changed and recompiled without requiring the recompilation of any client code that depends on it, as long as the specification remains unchanged.

Information Hiding with private Types

To provide encapsulation, a package can hide the internal structure of a type from its clients. This is achieved by declaring a type as private in the visible part of the package specification. The full definition of the type is then deferred to a special private section at the end of the specification.

-- Specification file: stack.ads
package Stack is
  type T is private; -- The client sees only the name 'T'

  -- Operations on the private type
  procedure push     (S : in out T; Value : in Integer);
  function  pop      (S : in out T) return Integer;
  function  is_empty (S : T) return Boolean;

private
  -- The full definition is hidden from the client
  MAX_SIZE : constant := 100;
  subtype Index is Integer range 0 .. MAX_SIZE;
  type Integer_Array is array (Positive range <>) of Integer;

  type T is record
    elements : Integer_Array (1 .. MAX_SIZE);
    top      : Index := 0;
  end record;
end Stack;

A client of this package can declare variables of type Stack.T and call the push and pop procedures, but it cannot directly access the elements or top fields of the record. This enforces the abstraction and prevents the client from corrupting the internal state of the stack.

5.3 Generic Units

Generics provide a high level of abstraction in Ada, allowing for the creation of reusable “template” subprograms and packages that are parameterized by types or other program entities. This enables the development of reusable, type-safe algorithms and data structures.  

A generic unit is not directly usable. It must first be instantiated by providing actual parameters for its formal generic parameters. This instantiation, which uses the new keyword, creates a new, concrete subprogram or package that can then be used like any other.

-- Generic specification for a swap procedure
generic
  type Item is private; -- A generic formal type parameter
procedure swap (left, right : in out Item);

-- Generic body
procedure swap (left, right : in out Item) is
  temp : Item := Left;
begin
  left  := right;
  right := temp;
end swap;

-- Instantiation for Integer
procedure swap_integers is new swap (Item => Integer);

-- Instantiation for a user-defined record type
procedure swap_persons is new swap (Item => Person);

Ada’s generic formal parameters provide a rich set of options, which act as a contract between the generic unit and its instantiator. The generic unit can specify precisely what properties its parameters must have. For example, a generic sorting algorithm can require that its element type has a “<” operator defined. The compiler will then ensure that the generic is only instantiated with types that fulfill this contract, preventing errors at compile time.

6. Advanced Iteration and Container Design

Chapter 4 introduced the fundamental iterative statements that are necessary for controlling program flow. While those constructs are sufficient for many programming tasks, Ada’s iteration framework is far more extensive, providing a functionally rich and uniform syntax for traversing data structures of arbitrary complexity. This capability is central to the language’s support for abstraction and the creation of reusable software components.

This chapter examines in detail the advanced iteration patterns that enable this flexibility. We will explore generalized and procedural iterators, which offer alternative models for controlling loop execution. Subsequently, we will examine the specific language features—primarily interfaces and aspects—that allow programmers to design their own custom container types that integrate directly with Ada’s for...of... loop syntax. Understanding these mechanisms is an essential component of leveraging the full capabilities of Ada’s type system and abstraction capabilities.

6.1 The Iterator Pattern in Ada

The iterator is a well-established software design pattern that provides a standard way to access the elements of a collection sequentially without exposing its underlying representation. This separation of traversal logic from data structure implementation is a fundamental principle of modern, component-based software engineering.

Ada formalizes this pattern directly within the language. Instead of relying solely on library conventions, Ada provides dedicated syntax (for ... in ... and for...of...) and a standardized set of interfaces (defined in the Ada.Iterator_Interfaces package) to create and consume iterators. This language-integrated approach offers demonstrable benefits:

  • Uniformity: All collections, from simple arrays to complex user-defined data structures like trees or sparse matrices, can be traversed using the same simple for loop syntax.
  • Abstraction: A user of a container type does not need to know how the data is stored internally to iterate over its elements.
  • Type Safety: The entire iteration process is subject to Ada’s strong type checking, preventing common errors related to iterators in other languages.

The following sections will detail the different forms of iterators and the mechanisms for creating types that support them.

6.2 Generalized Iterators in Depth

The for...of... loop, introduced in Chapter 4, provides a direct way to iterate over a container using its default iteration behavior. However, a container may offer multiple ways to traverse its elements. For these cases, Ada provides the generalized iterator form, which uses the for ... in ... syntax.

This construct operates on an explicit iterator object, which is typically returned by a function call. This allows a container to provide a set of iterator-producing functions, each offering a different traversal strategy (e.g., forward, reverse, by key, or by value).

Syntax: for loop_parameter in iterator_object loop

Example: Consider a simple list that can be iterated both forwards and backwards. The Simple_List package can provide two distinct functions, Forward and Reverse, each returning a corresponding iterator object.

-- A package defining a simple iterable list.
package Simple_List_Package is
  type List is private;
  type Cursor is private;

  -- Operations to build the list
  procedure Append (Container : in out List; Value : Integer);
  function To_List (Values : array of Integer) return List;

  -- Iterator interfaces defined in a separate generic instantiation
  package Iter is new Ada.Iterator_Interfaces (Cursor, Has_Element);
  type Reversible_List_Iterator is new Iter.Reversible_Iterator with null record;

  function Forward (Container : List) return Reversible_List_Iterator;
  function Reverse (Container : List) return Reversible_List_Iterator;

private
  -- Full private definitions for List, Cursor, and iterator functions...
end Simple_List_Package;

A client can then use these explicit iterators to control the direction of the loop.

with Ada.Text_IO;
with Simple_List_Package;

procedure demonstrate_generalized_iterator is
  use Simple_List_Package;
  my_list : constant List := To_List ([10, 20, 30]);
begin
  Ada.Text_IO.put_line ("Forward Iteration:");
  for item in Forward (my_list) loop
    Ada.Text_IO.put (Integer'image (item));
  end loop;
  Ada.Text_IO.new_line;

  Ada.Text_IO.put_line ("Reverse Iteration:");
  for item in Reverse (my_list) loop
    Ada.Text_IO.put (Integer'image (item));
  end loop;
  Ada.Text_IO.new_line;
end demonstrate_generalized_iterator;

This approach gives the programmer precise control over the iteration method while maintaining the clarity and safety of the for loop syntax.

6.3 Procedural Iterators

The procedural iterator offers a fundamentally different model of iteration based on an “inversion of control.” Instead of the loop retrieving data from a collection, the loop provides its body to an “iterating procedure,” which then controls the execution of that body the appropriate number of times with the correct values.

Syntax: for (loop_parameters) of iterating_procedure_call loop

Semantics: As specified in ARM 5.5.3, this syntax is semantically equivalent to the following transformation:

  1. The sequence_of_statements inside the loop is encapsulated within a locally declared procedure, P. The formal parameters of P match the loop_parameters.
  2. The iterating_procedure is then called with the access value P'Access as one of its arguments.

This pattern is especially effective for traversing abstract data sources that are not traditional collections, such as environment variables or directory contents.

Example: The standard library procedure Ada.Environment_Variables.Iterate is a procedural iterator. It accepts an access to a procedure that takes two String parameters (name and value).

with Ada.Text_IO;
with Ada.Environment_Variables;

procedure Show_Environment is
begin
  Ada.Text_IO.put_line ("Environment Variables:");
  for (name, value) of Ada.Environment_Variables.Iterate loop
    Ada.Text_IO.put_line (name & " = " & value);
  end loop;
end Show_Environment;

Here, the loop body is implicitly packaged as a procedure and passed to Ada.Environment_Variables.Iterate, which then calls it for each environment variable it finds. This concise syntax abstracts the complexity of access-to-procedure types from the user of the iterator.

6.4 Designing Iterable Container Types

The direct syntax of the for...of... loop relies on the container type providing a specific set of interfaces and aspects that the compiler can use. This section outlines the mechanisms required to make a user-defined type an “iterable container.”

6.4.1 The Ada.Iterator_Interfaces Package

This standard library package provides the foundational components for all iterators in Ada. It is a generic package that is instantiated with a Cursor type, which represents a position within a collection. It provides several key interface types that a custom iterator must implement:

  • Forward_Iterator: For iterators that can only move forward.
  • Reversible_Iterator: For iterators that can move both forward and backward.
  • Parallel_Iterator: For iterators that support parallel traversal (as discussed in Chapter 4).

6.4.2 The Default_Iterator and Iterator_Element Aspects

To make a container type compatible with the simple for...of... loop, two aspects must be specified:

  • Default_Iterator: This aspect specifies a function that returns the default iterator object for the container. The compiler calls this function implicitly at the start of a for...of... loop.
  • Iterator_Element: This aspect specifies the subtype of the elements that the iterator yields. This becomes the type of the loop parameter.

Conceptual Declaration:

package My_Containers is
  type My_Type is tagged private;
  subtype Element_Type is Integer;

  function Create_Iterator (Container : My_Type) return Iterator_Object;

  -- Applying the aspects to make My_Type iterable
  type My_Type is tagged private with
    Default_Iterator => Create_Iterator,
    Iterator_Element => Element_Type;

private
  -- ...
end My_Containers;

6.4.3 The Constant_Indexing and Variable_Indexing Aspects

Once the iterator provides a Cursor position, the loop needs a mechanism for retrieving the element at that position. This is implemented through indexing aspects:

  • Constant_Indexing: Specifies a function that takes the container and a cursor and returns the corresponding element for read-only access. This is required for iterating over constants.
  • Variable_Indexing: Specifies a function that takes the container and a cursor and returns a reference to the element, allowing the loop body to modify it. This is required for iterating over variable containers where element modification is desired.

By defining a container type with these aspects, programmers can create fully-featured, reusable components that integrate coherently and safely into the core iteration constructs of the Ada language.

6. Exception Handling

Ada provides a built-in exception handling mechanism to manage errors and other unexpected events that occur during program execution. A key principle of this mechanism is the

separation of concerns: the code that detects an error is separate from the code that handles it.

In Ada, exceptions are declared as named objects, not types.

-- In a package specification
Invalid_Input : exception;

When an anomalous situation is detected, an exception is raised using the raise statement. This immediately suspends normal program execution and begins propagating the exception up the chain of subprogram calls.

procedure process_data (value : in Integer) is
begin
  if value < 0 then
    raise Invalid_Input with "Input value cannot be negative";
  end if;
  -- Normal processing...
end process_data;

An exception handler is defined within an exception block, which can be placed at the end of any begin ... end block (such as a subprogram body or a declare block).

begin
  -- Some sequence of statements
  process_data (value => User_Value);
exception
  when Invalid_Input =>
    Ada.Text_IO.put_line ("Error: Please provide a valid input.");
  when others =>
    Ada.Text_IO.put_line ("An unexpected error occurred.");
    raise; -- Re-raises the current exception to be handled by an outer scope
end;

This design encourages modular programming. A low-level utility package can detect an error and raise a specific exception, but it does not presume to know how the application should respond. It is the responsibility of the higher-level calling code to handle the exception in a way that is appropriate for the application’s overall state and requirements.

7. Interfacing with External Systems

While Ada is designed to build self-contained systems, it is also a language that provides for interoperability with existing code, particularly libraries written in C. Ada provides a standardized mechanism for this interoperability, defined in Annex B of the language standard. This mechanism is not an ad-hoc, compiler-specific feature but a portable part of the language itself, ensuring that interfacing logic can be written in a consistent manner across all compliant Ada compilers.

7.1 Interfacing with the C Language

The primary tool for C interoperability is the predefined library package Interfaces.C and its children, such as Interfaces.C.Strings and Interfaces.C.Pointers. These packages provide the types and subprograms necessary to interface between the two languages.

Type Mapping

A crucial first step is to map data types correctly. The Interfaces.C package provides a set of Ada types that are guaranteed to have the same size and representation as their C counterparts on a given platform. This prevents data corruption at the language boundary.

Importing C Subprograms and Variables

To call a C function from Ada, you declare a corresponding Ada subprogram and mark it for import. This is done using the with import => True, convention => c aspect (or the older pragma import). The convention => c part tells the Ada compiler to use the C calling convention for passing parameters and handling the return value.

// In a C header file, my_lib.h
int multiply_by_two (int value);
-- In an Ada source file
with Interfaces.C; use Interfaces.C;

procedure call_c_function is
  -- Declare an Ada function that maps to the C function
  function multiply_by_two (value : int) return int
    with import        => True,
         convention    => c,
         external_name => "multiply_by_two"; -- The name of the C function

  result : int;
begin
  result := multiply_by_two (10); -- Calls the C function
end call_c_function;

C global variables can be imported in a similar way.

Exporting Ada Subprograms to be Called from C

The reverse is also possible. An Ada subprogram can be made callable from C code by using the with export => True, convention => c aspect. The Ada compiler will generate a function with C-compatible linkage that can be called from any C module.

-- In an Ada package specification
package Ada_Library is
  function add (a, b : int) return int
    with export        => True,
         convention    => c,
         external_name => "ada_add";
end Ada_Library;

Managing the Safety Boundary

The design of the interfacing packages reflects Ada’s safety-oriented design. They provide tools to manage the transition between Ada and C. This is most evident in the handling of strings and pointers.

C-style strings are simple null-terminated arrays of characters (char*), a common source of buffer overflow vulnerabilities. Ada’s native String type, by contrast, is a bounded object that always knows its own length. The Interfaces.C.Strings package provides functions to safely convert between these two representations (to_c, to_ada). It also provides a special access type, chars_ptr, for handling C strings, and functions like value to convert a chars_ptr into a safe Ada String.  

This design forces the programmer to be conscious of the language boundary. Unsafe C constructs are handled at the boundary. For instance, if the value function is passed a null C pointer, it does not cause a crash or undefined behavior; instead, it raises a handleable Ada exception. In this way, Ada attempts to apply its safety semantics at the interface with C code, managing the boundary and containing risks.  

8. Concurrency and Real-Time Programming

A characteristic of Ada is its built-in support for concurrency, also known as parallel processing. Unlike many languages that rely on external libraries (like pthreads in C) or platform-specific APIs, Ada’s concurrency features are an integral part of the language specification. This has implications for portability, safety, and correctness. A concurrent Ada program is portable to any platform with a compliant compiler, from a small bare-metal embedded system to a multi-core server, and the concurrency semantics are guaranteed to be consistent. The compiler is aware of the concurrency constructs and can perform checks and optimizations that are impossible with library-based approaches.

Ada provides a spectrum of concurrency models, offering different tools suited for different points in the trade-off space between expressiveness, performance, and formal analyzability.

8.1 The Tasking Model

The fundamental unit of concurrency in Ada is the task. A task is an independent thread of control that executes concurrently with other tasks in the program. Like a package or subprogram, a task is defined in two parts: a task specification, which defines its public interface, and a task body, which contains its executable code.

Tasks are activated automatically. When the program’s execution enters the scope where a task is declared, the task begins its execution in parallel with the code that declared it (its “master”). A scope (such as a subprogram or a block) will not be exited until all tasks declared within it have completed their execution. This provides a simple and robust form of synchronization.

with Ada.Text_IO;

procedure demonstrate_tasking is
  task T; -- Task specification (no public interface here)

  task body T is -- Task implementation
  begin
    for i in 1 .. 5 loop
      Ada.Text_IO.put_line ("Task T is running...");
    end loop;
  end T;

begin -- Main procedure begins execution
  Ada.Text_IO.put_line ("Main procedure is running.");
  -- Main procedure will now wait here until task T terminates
end demonstrate_tasking;

Ada also supports task types, which allow for the declaration of multiple tasks from a single template, analogous to creating multiple objects from a class. This is useful for creating arrays of worker tasks, for example.

8.2 Synchronization and Communication: The Rendezvous

The original mechanism in Ada for direct, synchronous communication and synchronization between tasks is the rendezvous. It is a model for client-server style interactions.

The rendezvous is built on two constructs:

  • entry: An entry is declared in a task’s specification and acts as a publicly callable entry point, similar to a procedure. It defines the interface for the interaction.

  • accept: An accept statement is placed in the task’s body. When the task’s execution reaches an accept statement, it waits until another task calls the corresponding entry.

When a client task calls an entry and the server task is at the matching accept statement, the two tasks are synchronized in a rendezvous. The client task is blocked while the server task executes the code within the do ... end block of the accept statement. This block can be used to exchange data via the entry’s parameters. Once the server completes the accept block, the rendezvous is finished, and both tasks continue their execution independently.

task Server is
  entry request_data (value : out Integer);
end Server;

task body Server is
  current_value : Integer := 0;
begin
  loop
    current_value := current_value + 1;
    accept request_data (value : out Integer) do
      value := current_value; -- This happens during the rendezvous
    end request_data;
  end loop;
end Server;

-- In a client task:
declare
  my_data : Integer;
begin
  Server.request_data (my_data); -- Client calls entry, blocks until rendezvous is complete
end;

To handle more complex situations, a task can use a select statement to wait for calls on multiple entries at once, to time out if no call arrives within a certain period (or delay), or to take an alternative action if no rendezvous is immediately possible (else).

8.3 Protected Objects for Shared Data

While the rendezvous can be inefficient for the common problem of managing access to shared data, Ada 95 introduced protected types. A protected object is a passive data structure that encapsulates private data and provides guaranteed mutually exclusive access to it. This is a direct and efficient solution to the critical section problem.

A protected object provides three kinds of operations:

  • Procedures: Provide exclusive read-write access to the private data. The language runtime ensures that only one task can be executing a procedure of a given protected object at any time.

  • Functions: Provide shared read-only access to the private data. Multiple tasks are allowed to execute functions of the same protected object concurrently.

  • Entries: Similar to task entries, but with a boolean barrier condition. A task calling a protected entry is queued and blocked until the entry’s barrier evaluates to true. Access is still mutually exclusive.

protected type Shared_Counter is
  procedure increment;
  function get_value return Integer;
private
  value : Integer := 0;
end Shared_Counter;

protected body Shared_Counter is
  procedure increment is
  begin
    value := value + 1; -- Exclusive access guaranteed
  end increment;

  function get_value return Integer is
  begin
    return value; -- Shared read-only access
  end get_value;
end Shared_Counter;

With protected objects, the programmer declares the data and operations, and the compiler and runtime system handle the underlying locking mechanisms automatically. This prevents race conditions and deadlocks by construction, making concurrent programming safer and simpler.

8.4 The Ravenscar Profile

For safety-critical and hard real-time systems, the full set of Ada concurrency features can be too complex to be formally analyzed for timing behavior and schedulability. To address this, the Ravenscar Profile was developed and standardized in Ada 2005.

The Ravenscar Profile is a selected subset of Ada’s tasking features that is powerful enough for real-time programming but simple enough to be suitable for formal static analysis. Its restrictions typically include:

  • A fixed number of tasks, with no dynamic creation or termination.
  • A simplified task communication model, usually limited to protected objects.
  • No complex rendezvous or select statements with else parts.
  • Simple, predictable scheduling policies.

By adhering to this profile, developers can build concurrent systems for which they can prove properties like the absence of deadlocks and the meeting of hard deadlines. This makes it possible to certify Ada software for safety standards, such as DO-178C in avionics.

9. Design by Contract (DbC)

Standardized in Ada 2012, Design by Contract (DbC) enhances Ada’s reliability features by allowing programmers to embed formal, verifiable specifications directly into the source code. These contracts act as executable documentation, connecting high-level requirements to the implementation. When enabled by a compiler switch (-gnata), these contracts are checked at runtime, raising an Assert_Failure exception if violated.

The core components of DbC in Ada are:

  • Preconditions: A condition that must be true before a subprogram is called. It is an obligation on the part of the caller. Specified with with pre =>....

    function square_root (x : Float) return Float with pre => x >= 0.0;
    
  • Postconditions: A condition that the subprogram guarantees will be true upon its successful completion. It is an obligation on the part of the subprogram’s implementation. Specified with with post =>.... Postconditions can refer to the initial value of parameters with the 'old attribute and the function’s result with the 'result attribute.

    procedure increment (value : in out Integer) with post => value = value'old + 1;
    
  • Type Invariants and Predicates: Conditions that must hold true for all objects of a given type. A predicate (with static_predicate => ...) is a property that is checked whenever a value of the type is created or modified. An invariant (with type_invariant => ...) is a property of a private type that is checked at the boundaries of its public operations.

Contracts are a useful tool. They provide documentation that is consistent with the compiled code. They serve as a precise basis for testing and debugging, and they are a critical input for formal verification tools.

10. Introduction to SPARK

SPARK (a portmanteau of “SPADE Ada Kernel”) is a formally analyzable subset of the Ada language designed specifically for the development of high-assurance software.

SPARK achieves its goal of enabling proof of correctness by a combination of two strategies:

  1. Language Subsetting: It excludes features of Ada that are difficult to analyze formally, such as general access types (to prevent aliasing), exception handling (as all runtime errors are proven to be absent), and side effects in functions.

  2. Enhanced Contracts: It extends Ada’s DbC model with more detailed contracts that can specify properties like data dependencies (which inputs affect which outputs), information flow policies, and state abstraction.

Using a toolset like GNATprove, a developer can use formal methods to prove, with a high degree of assurance through mathematical proof, that a SPARK program is free from all language-defined runtime errors, that it conforms to its functional specifications, and that it adheres to critical safety and security properties. This represents a high level of software assurance, moving beyond testing (which can only show the presence of bugs) to proof (which can show their absence). This layered approach—from the strong type system, to runtime checks, to Design by Contract, to formal proof with SPARK—is what makes the Ada language family suited for building high-integrity software.

Appendix: Clair Coding Style Guide

The Clair Coding Style Guide provides modern conventions for Ada by adapting common styles from other major programming languages to fit Ada’s features. The guide primarily uses snake_case for variables and subprograms, and Pascal_Case with underscores for types. This creates a style that is intuitive for a wide range of developers while maintaining clarity.


Indentation: Use 2 spaces for indentation, not tabs.

  • Rationale: Ada’s explicit and often verbose syntax can lead to deeply nested code blocks. A 2-space indent provides clear visual structure without consuming excessive horizontal space, which helps prevent lines from becoming overly long and difficult to read.

Reserved Words & Aspects: Use snake_case (all lowercase).

  • Rationale: This aligns with the near-universal convention in major programming languages (such as Python, C, C++, and Java) where language keywords are written in lowercase. Adopting this common practice improves readability and makes the code more intuitive for developers familiar with other languages.
  • Example: package, is, begin, end, if, procedure, with, pre, post

Pragmas: Use snake_case (all lowercase) for the pragma name and its convention identifier.

  • Rationale: This aligns with a predominant convention in influential languages like C, C++, and Python, where compiler directives are styled in lowercase. Adopting this widely recognized practice improves familiarity and readability for developers. This applies to both the pragma itself (e.g., import) and standard convention identifiers (e.g., c, intrinsic).
  • Example: pragma import (c, my_c_func, "my_c_func"), pragma convention (c, My_Data_Type)

Spacing:

  • Subprogram Calls & Declarations: Use a single space between the subprogram name and the opening parenthesis (.
    • Rationale: The primary purpose of this rule is to improve readability by creating a clear visual separation between a subprogram’s name and its parameter list. This convention offers the additional benefit of enabling precise, whole-word searches. A tool can search for subprogram_name (the name followed by a space) to reliably locate all call sites without matching other identifiers that share the same prefix.
    • Example (Call): Clair.Error.get_error_message (errno_code);
    • Example (Declaration): procedure exit_process (status : Integer := EXIT_SUCCESS);
  • Array Indexing: Do not use a space between the array name and the opening parenthesis (.
    • Rationale: To visually differentiate array indexing from subprogram calls, which require a space.
    • Example: all_bids(n), my_matrix(row, col)
  • Range Operator (..): Use a single space on both sides of the range operator.
    • Rationale: To visually separate the operator from the range bounds, which prevents confusion and enhances readability, especially when used with floating-point or fixed-point literals.
    • Example (Type Declaration): range 0.0 .. 100.0
    • Example (Loop): for i in 1 .. 10 loop

Variables, Subprograms, & Entries: Use snake_case (all lowercase with underscores).

  • Rationale: To maintain a consistent and readable style for all user-defined, executable, or data-holding identifiers.
  • Example (Variables & Subprograms): my_variable, get_pid
  • Example (Entries): get_item, put_message
    protected body Buffer is
      entry get_item (item : out Data) when not is_empty is
        -- ...
      end get_item;
    end Buffer;
    
  • Return Value Variables: For variables holding a subprogram’s return value, especially status codes (e.g., 0, -1), prefer using retval.
    • Rationale: This is a widely understood convention that avoids potential conflicts with the result identifier. For return values representing specific data, use a more descriptive name (e.g., bytes_written, new_fd).
    • Example: retval := dlfcn_h.dlclose (self.handle);

Attributes: Use snake_case (all lowercase).

  • Rationale: To maintain stylistic consistency with functions and variables. Attributes often act as functions (e.g., 'image returns a value) or represent a property like a variable (e.g., 'length). As both subprograms and variables use snake_case in this guide, styling attributes the same way ensures consistency across these related elements.
  • Example: errmsg'length, c_path'address

Types, Subtypes, Exceptions & Protected Objects: Use Pascal_Case, where each word is capitalized and consecutive words are separated by an underscore.

  • Rationale: This convention improves the readability of type names and visually distinguishes them from other identifiers, such as snake_case variables and subprograms.
  • Avoid Redundancy: Do not repeat the package name as a prefix for types declared within that package. The context provided by the package name is sufficient.
    • Rationale: Repetitive naming, such as File.File_Descriptor, is verbose and reduces readability. Therefore, this guide recommends the concise form, File.Descriptor.
  • Example: Library_Load_Error, Symbol_Lookup_Error, Descriptor, Flags

Loops & Goto Labels: Use Pascal_Case.

  • Rationale: This rule adapts a common convention from other programming languages where PascalCase is used for labels. The style is modified to include underscores to align with Ada’s idiomatic use of underscores for readability in multi-word identifiers.
  • Example (Loop Name):
    Main_Process_Loop:
      loop
        -- ...
        exit Main_Process_Loop when condition;
      end loop Main_Process_Loop;
    
  • Example (Goto Label):
    if has_error then
      goto Error_Handler;
    end if;
    -- ...
    <<Error_Handler>>
    log_error (error_code);
    

Constants:

  • Compile-Time Constants: Use UPPER_CASE_WITH_UNDERSCORES. This rule applies to all static constants, including those from the standard library.
    • Rationale: To clearly distinguish static, fixed values from all other identifiers.
    • Project-Defined Example: EXIT_SUCCESS, MAX_BUFFER_SIZE
    • Standard Library Example: System.NULL_ADDRESS, Interfaces.C.NUL, Interfaces.C.Strings.NULL_PTR
  • Runtime Constants: Use snake_case (like variables).
    • Rationale: Used for constants within a subprogram that are initialized with a dynamic value (e.g., from a parameter). Treat these as ‘read-only variables’.
    • Example: final_message : constant String := "Error: " & message;

Packages: Use Pascal_Case.

  • Example: Clair.Process
  • Exception: In the case of Dl, which consists of two letters, it is written as DL. (e.g., Clair.DL, not Clair.Dl which can look like Clair.D1).

Standard Library Naming:

  • Interfaces.C: Types and subprograms from the Interfaces.C package and its children should use snake_case to match the C standard library’s naming convention. Constants from this package follow the global UPPER_CASE rule for compile-time constants.
    • Rationale: To maintain a clear and consistent mental mapping between Ada and C, while ensuring all constants in the project have a uniform appearance.
    • Example (Types/Subprograms): Interfaces.C.int, Interfaces.C.char_array, Interfaces.C.Strings.chars_ptr
    • Example (Constants): Interfaces.C.NUL, Interfaces.C.Strings.NULL_PTR

  1. Whitaker, William A. “Ada - The Project, The DoD High Order Language Working Group.” ACM SIGPLAN Notices, vol. 28, no. 3, 1993, http://archive.adaic.com/pol-hist/history/holwg-93/holwg-93.htm