Ada Programming

Hodong Kim <hodong@nimfsoft.art>

Preface

This book is intended as a technical introduction to the Ada programming language. Its content is designed to be accessible to individuals with a background in programming, covering fundamental concepts and progressing to advanced topics such as concurrency and high-integrity systems. All code examples in this book conform to the Clair Coding Style Guide, which is included as an appendix. This guide establishes a consistent and readable format for all source code, adhering to a set of conventions adapted from various programming languages to fit the specifics of Ada.

The Ada language was originally developed with three primary design goals:

  • Reliability and Maintainability: Ada prioritizes program correctness and long-term viability. This is achieved through features such as strong typing, explicit declarations, and compile-time checks, which help prevent common programming errors. The syntax is designed for readability, reducing ambiguity and facilitating code review and maintenance.
  • Programming as a Human Activity: The language design acknowledges that software is developed and maintained by human programmers. It provides constructs that are intuitive and consistent, reducing cognitive load. Features such as packages and generic units support the composition of programs from independently developed components, which is a key aspect of modern software engineering.
  • Efficiency: Ada was designed to be efficient across various platforms. The language was developed to avoid constructs that would necessitate an over-elaborate compiler or lead to inefficient use of resources, such as memory or CPU time. This design choice ensures that Ada can be used in resource-constrained environments and for applications where performance is critical.

The subsequent revisions of the Ada 1995 standard, including Ada 2022, have expanded upon these foundational principles. Ada 2022 introduces features such as enhanced support for concurrency, improved type flexibility, and standardized packages for various application areas, all while maintaining the language’s core emphasis on reliability, maintainability, and efficiency.


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


Table of Contents

Part I: Foundations of Ada

This section provides a comprehensive introduction to the fundamental principles that define the Ada programming language. The content explores the historical context of its development, its core design philosophy, and the unique features that make it suitable for building reliable and maintainable systems. By understanding these foundations, the reader will be equipped to appreciate how Ada’s design choices influence every aspect of the language, from its syntax to its concurrency model. This part serves as a conceptual cornerstone for the practical programming topics covered in later sections of the book.

1. The Ada Paradigm

This chapter introduces the fundamental concepts and design philosophy of the Ada programming language. We will explore the historical context that led to its creation and examine the core principles of reliability, maintainability, and efficiency. This discussion will provide a foundation for understanding Ada’s role in developing high-integrity systems. The chapter concludes with an overview of the key features of Ada 2022 that have extended and refined these foundational principles.

1.1 A History of Ada

The genesis of the Ada programming language lies in a software engineering crisis identified by the United States Department of Defense (DoD) during the 1970s. The department’s embedded computer systems were being developed using a multitude of programming languages—over 450 different languages and dialects were in use by 1983. This lack of commonality created significant challenges related to software cost, reliability, maintainability, and programmer training. Many of these languages were dependent on specific hardware, were not formally specified, and lacked features to support reliable, modular programming.

To address these issues, the DoD established the High Order Language Working Group (HOLWG) in January 1975. The group was charged with establishing a single, common programming language suitable for the department’s real-time, embedded applications. From its inception, the project was conducted as an open, international collaboration involving government agencies, industrial partners, and academic institutions from North America and Europe.

The Requirements Process

The HOLWG undertook a rigorous, iterative process to define the technical requirements for the new language. This effort produced a series of widely circulated documents, each building upon the feedback from the previous one. This systematic approach ensured that the final requirements were well-defined, feasible, and aligned with the needs of the user community. The key documents in this series were:

  • STRAWMAN (April 1975): An initial proposal to stimulate discussion.
  • WOODENMAN (August 1975): A more refined set of criteria.
  • TINMAN (January 1976): A version that consolidated requirements from across the DoD.
  • IRONMAN (January 1977): A formal specification intended to guide language design.
  • STEELMAN (June 1978): The final and most definitive set of requirements.

A critical outcome of this process was the conclusion that a single language could, in fact, meet the diverse needs of nearly all DoD applications. After evaluating numerous existing languages against the TINMAN requirements, the HOLWG determined that none were suitable, but that developing a new language to meet the specifications was achievable.

Design and Selection

In 1977, the DoD launched a competitive design effort, awarding contracts to four teams to produce preliminary language designs based on the IRONMAN requirements. To ensure impartiality during evaluation, the designs were identified only by color:

  • Red: Intermetrics
  • Green: Cii-Honeywell Bull
  • Blue: SofTech
  • Yellow: SRI International

All four teams chose the Pascal language as a conceptual starting point. Following an extensive international review, the Green and Red designs were selected in 1978 to proceed to a final design phase. In May 1979, the Green design, led by Jean Ichbiah of Cii-Honeywell Bull, was chosen as the winner.

The language was named Ada in honor of Augusta Ada King, Countess of Lovelace (1815–1852). A 19th-century mathematician, she collaborated with Charles Babbage on his Analytical Engine and is widely considered to be the first computer programmer. As a further tribute, the original military standard for the language was designated MIL-STD-1815, referencing her birth year.

Standardization and Continued Evolution

After its selection, the Ada design underwent a period of public test and evaluation. The initial reference manual was published in 1979, and after incorporating feedback, the language was standardized as ANSI/MIL-STD-1815 in 1980.

Ada has since been maintained and evolved through a formal international standardization process. This ensures that the language remains technically current while preserving its stability for long-lived projects. The major revisions of the standard are:

  • Ada 83: The initial standard (ANSI/MIL-STD-1815A), later adopted as ISO 8652:1987.
  • Ada 95: A major revision that introduced full support for object-oriented programming, making Ada the first ISO-standardized object-oriented language.
  • Ada 2005: An amendment that added features such as programming by interface, improved real-time support, and additions to the standard library.
  • Ada 2012: This version introduced contract-based programming (preconditions and postconditions), expression functions, and enhanced support for multicore parallel programming.
  • Ada 2022: The current standard, which includes further refinements to its features for high-integrity software development.

This history of rigorous engineering, open review, and controlled evolution underpins Ada’s suitability for developing large-scale, long-lived, and high-integrity systems.

Ada Development and Standardization Timeline.

Date Milestone
Jan 1975 Formation of the High Order Language Working Group (HOLWG).
Apr 1975 The STRAWMAN requirements document is issued.
Aug 1975 The WOODENMAN requirements document is issued.
Jan 1976 The TINMAN requirements document is issued.
Jan 1977 The IRONMAN requirements document is issued.
Aug 1977 Competitive language design contracts are awarded.
Jun 1978 The STEELMAN requirements document is issued.
May 1979 The final language design (the “Green” proposal) is selected.
Dec 1980 The language is standardized as MIL-STD-1815.
Feb 1983 The revised standard, ANSI/MIL-STD-1815A (Ada 83), is approved.
Apr 1983 The first compiler is validated against the Ada 83 standard.
Jun 1987 Ada 83 is adopted as international standard ISO 8652:1987.
Feb 1995 Ada 95 is approved as international standard ISO/IEC 8652:1995.
Mar 2007 Ada 2005 is published as an amendment to the Ada 95 standard ISO/IEC 8652:1995/Amd 1:2007.
Dec 2012 Ada 2012 is approved as international standard ISO/IEC 8652:2012.
May 2023 Ada 2022 is approved as international standard ISO/IEC 8652:2023.

This table outlines the key events in the history of Ada, from the formation of the HOLWG to the publication of the current Ada 2022 standard.

1.2 Core Design Philosophy

Ada’s design philosophy is centered on three core tenets: reliability and maintainability, treating programming as a human activity, and efficiency.

1.2.1 Reliability and Maintainability

Ada was designed with an emphasis on program reliability and long-term maintainability. These two attributes are interconnected and were central to the language’s initial development. The design prioritizes the detection of errors at compile time rather than at runtime, which contributes to the development of robust and dependable software.

To achieve this, the language incorporates a number of features. First, Ada’s strong typing requires all variables to be explicitly declared with a specific type. The compiler uses this information to enforce type safety, ensuring that operations are compatible with the properties of the objects being manipulated. This reduces the risk of type-related errors, which are common in languages with weaker type systems.

Second, the syntax of Ada is designed for readability. English-like constructs are used in place of encoded or abbreviated forms, which makes the source code more accessible to human review and analysis. This design choice simplifies the process of code inspection and maintenance, as the intent of the code is more directly expressed.

Third, Ada provides built-in support for separate compilation. This feature allows large programs to be composed of independently developed and compiled units. The compiler can still perform full consistency checks between these units, which ensures that interfaces and dependencies are correctly managed across the entire system. This structure is essential for large-scale software projects, as it simplifies both the development and maintenance of individual components without compromising overall system integrity.

The language’s support for exceptions provides a structured mechanism for handling runtime errors. This prevents program termination due to unexpected conditions and allows for the implementation of robust error recovery strategies. The emphasis on these design elements reflects a core principle of Ada: promoting code that is not only correct upon initial deployment but also resilient and easily adaptable to future changes.

1.2.2 Programming as a Human Activity

The design of Ada acknowledges that software development is a human-centered process. The language was created to optimize the interaction between the programmer and the code base, aiming for clarity and ease of use in a professional context. This objective is achieved through several design principles that collectively support the human aspect of software engineering.

A primary principle is the focus on readability over writability. The language syntax and semantics are structured to be intuitive and self-documenting. Constructs like the use of full words for reserved keywords (e.g., and then, or else) and clear, explicit declarations make programs easier to understand for multiple developers over time. This approach minimizes the cognitive load associated with reading and comprehending code, which is critical for collaborative projects and long-term maintenance.

The language also promotes conceptual simplicity by integrating a relatively small number of core concepts in a consistent manner. This systematic integration avoids the need for developers to learn a large number of special cases or exceptions. For instance, the concept of a package provides a unified mechanism for managing namespaces, encapsulating data, and defining modular program structures. This consistency allows developers to apply a limited set of rules across diverse programming tasks.

Furthermore, Ada’s design emphasizes the ability to assemble programs from independent components. The package concept, combined with features such as private types and generic units, directly supports this modular approach. This design enables the construction of systems from pre-existing, tested software modules, which is a fundamental practice in modern software engineering. The hierarchical library structure and type extension capabilities introduced in later revisions further support the evolution and maintenance of these components with minimal disruption to existing code. This allows for the development of adaptable software systems that can be modified and extended as requirements change, without compromising the integrity of trusted parts.

1.2.3 Efficiency

Ada’s design prioritizes efficiency in both execution time and memory usage. Every language construct was scrutinized to ensure it could be implemented effectively on contemporary computer architectures without requiring excessive resources. The objective was to avoid language features that would inherently lead to slow or inefficient compilers or generate code with significant performance overhead.

The language’s strong typing system, for example, allows compilers to perform comprehensive checks and optimizations at compile time. By catching errors before execution, the need for extensive runtime checks is reduced, which can improve performance. Additionally, the language’s design avoids mechanisms that require complex, unpredictable runtime support, contributing to more deterministic and efficient execution.

Ada provides fine-grained control over data representation and memory layout. Features such as representation clauses allow a programmer to specify the exact memory size and alignment of data types, including records and enumerations. This capability is essential for embedded and systems programming, where direct interaction with hardware and efficient memory use are paramount.

The language also provides constructs for concurrency that are designed for efficient implementation. For instance, the task and protected object mechanisms offer structured, high-level abstractions for parallel programming. These constructs are supported by efficient runtime systems that can leverage modern multi-core architectures effectively, as the language standard includes specific provisions for parallel constructs like parallel for loops and parallel blocks (Ada 2022 Reference Manual, Section 5.5, 5.6.1). This ensures that Ada programs can make safe and efficient use of concurrent hardware resources.

1.3 Suitability for High-Integrity Systems

Ada is a language designed for the development of high-integrity systems. A high-integrity system is one in which the failure to operate correctly can result in severe consequences, such as loss of life, significant financial loss, or environmental damage. Examples of such systems include avionics, railway signaling, medical devices, and power plant control. The language’s suitability for these domains stems from its core design philosophy, which emphasizes reliability, maintainability, and safety. This section details the specific features and properties that make Ada an appropriate choice for high-integrity software.

Strong Typing and Safety

The strong typing of Ada is a fundamental mechanism for preventing many common programming errors. Every object has a type that defines its set of possible values and applicable operations. The compiler enforces these type rules rigorously, ensuring that operations are performed only on compatible types. This approach eliminates implicit type conversions that can lead to data corruption or unexpected behavior.

Furthermore, Ada’s type system allows for the definition of subtypes with specific constraints, such as a limited range of values. The compiler inserts run-time checks to verify that an object’s value remains within its defined range. If a constraint is violated, a predefined exception, such as Constraint_Error, is raised. This behavior ensures that errors are detected and handled predictably, rather than leading to silent data corruption or unpredictable program states.

Concurrency and Predictability

For real-time and embedded systems, predictable and reliable concurrency is a critical requirement. Ada provides built-in language features for tasking and synchronization, which are defined in Part VI of this book. These features include task types for concurrent execution and protected objects for controlled access to shared data.

Protected objects enforce a strict mutual exclusion protocol, which prevents multiple tasks from simultaneously accessing shared data in an inconsistent state. This design mitigates the risk of race conditions, deadlocks, and other concurrency-related issues that are difficult to debug. The language’s select statement provides mechanisms for non-blocking communication and timed entries, which are essential for building responsive real-time systems.

Specialized language subsets and tools, such as the Ravenscar Profile, further enhance Ada’s suitability for hard real-time systems. The Ravenscar Profile is a set of language restrictions that, when applied, guarantee a highly predictable and deterministic execution model, allowing for schedulability analysis and formal verification of timing properties.

Contract-Based Programming and Formal Verification

Ada 2022 includes extensive support for contract-based programming, a methodology that formalizes the behavior of software components. Through aspects like pre (precondition) and post (postcondition), developers can specify formal contracts for subprograms. The compiler and static analysis tools can then verify that these contracts are met.

  • A precondition specifies what must be true before a subprogram is called.
  • A postcondition specifies what must be true after the subprogram has completed its execution.

Example:

procedure transfer_money (from_account, to_account : in out Account;
                          amount                   : in     Money)
   with pre  => from_account.balance >= amount
             and then to_account'valid and from_account'valid,
        post => from_account.balance = from_account.balance'old - amount
             and then to_account.balance = to_account.balance'old + amount;

This example specifies that transfer_money requires a sufficient balance in from_account and guarantees that the balances are updated correctly. These contracts can be checked at compile-time or run-time, providing an additional layer of assurance.

The SPARK language subset, which is based on Ada, extends this concept to provide full formal verification. By adhering to a restricted set of Ada features, SPARK programs can be mathematically proven to be free of run-time errors like division by zero, buffer overflows, and null pointer dereferencing. This capability is highly valuable for certifying safety-critical software where traditional testing methods are insufficient.

1.4 Key Features of Ada 2022

The Ada 2022 standard, designated as ISO/IEC 8652:2023, represents a significant evolution of the language, building upon its core principles while introducing new features to address contemporary software engineering requirements. The enhancements are primarily focused on improving support for parallel computing, modernizing language syntax for clarity, and expanding capabilities for high-integrity systems.

Parallelism and Concurrency

The standard introduces native constructs for parallel execution, which simplify the development of applications for multicore architectures.

  • Parallel Loops and Blocks: The language now includes parallel for loops and parallel do...and...end do blocks, which allow a program to explicitly specify sequences of code to be executed concurrently.
  • Safety for Parallel Execution: New aspects such as Global and Global'Class provide a more precise mechanism for specifying subprogram interfaces, enabling compilers to detect potential data conflicts and ensure the safe execution of parallel constructs.
  • Real-Time Profiles: The Jorvik profile is introduced, offering a set of language restrictions that support the development of hard real-time applications with requirements beyond those of the existing Ravenscar profile.

Enhanced Language Constructs and Expressions

Ada 2022 refines the language’s syntax to improve expressiveness and readability in complex scenarios.

  • Shorthand for Assignment Targets: The token @ is a new shorthand for referring to the variable on the left-hand side of an assignment statement within the expression on the right. This simplifies expressions involving complex variable names or indexed components.
  • Generalized Iteration and Reduction: The concept of iteration has been generalized to include procedural iterators and iteration over user-defined container types. This is complemented by reduction expressions, which support map-reduce-style programming paradigms for efficiently processing data collections.
  • New Aggregate Forms: The language now supports delta aggregates for creating objects by incrementally updating existing ones, and container aggregates for direct construction of container objects.
  • Image Attribute for Nonscalar Types: The Image attribute, previously limited to scalar types, can now be used with nonscalar types. This is facilitated by the new Put_Image aspect, which allows a programmer to specify how a user-defined type is converted to a string.

Improved Contract-Based Programming and Library Features

The standard expands on the contract-based programming features introduced in Ada 2012 and provides a richer standard library.

  • Expanded Pre- and Post-conditions: Subprogram contracts can now be specified for generic formal subprograms and access-to-subprogram types. Additionally, the new Default_Initial_Condition aspect allows for specifying a postcondition for the default initialization of a type.
  • Refined Container Semantics: The behavior of many predefined container library operations is now formally specified using pre- and postcondition aspects. This increases the precision and predictability of the library.
  • Predefined Big Number Arithmetic: The standard library is extended with packages for arbitrary-precision integer and real arithmetic, addressing a need in domains that require computations with very large numbers.

2. Your First Ada Program

2.1 Setting Up the GNAT Toolchain

The GNAT (GNU Ada Toolchain) is a collection of development tools for the Ada programming language. It is a widely used, robust, and mature toolchain that provides a compiler, a debugger, and various utilities for building and managing Ada projects. The GNAT compiler supports all versions of the Ada standard, including the most recent Ada 2022.

The GNAT toolchain is available for a wide range of operating systems, including GNU/Linux, macOS, and Windows. It can be obtained through various methods:

  • System package managers: On most GNU/Linux distributions, GNAT can be installed using the native package manager (e.g., apt-get, dnf, pacman).
  • Source code: The source code for the GNAT toolchain is available from the GNU project, allowing for custom builds.
  • Commercial distributions: Vendors such as AdaCore provide commercial GNAT distributions with additional tools, support, and specialized features for specific application domains.
  • Free AdaCore distributions: AdaCore offers a free community-supported version of the GNAT toolchain for educational and non-commercial use.

After installation, the GNAT compiler (gnatmake or gnatprove) should be accessible from the command line. This can be verified by checking the version information. For example, on a UNIX-like system, executing gnatmake --version will display the installed version of the compiler.

The GNAT toolchain is often integrated with an Integrated Development Environment (IDE), such as GNAT Studio or a plugin for Visual Studio Code, to provide a more streamlined development experience. These environments offer features such as syntax highlighting, code completion, and integrated debugging, but they are not required for writing and compiling Ada programs. The command-line tools are sufficient for all development tasks.

2.2 Building Projects with GPRbuild

While it is possible to invoke the GNAT compiler directly to build a single source file, any project of moderate complexity benefits from a dedicated build tool. In the Ada ecosystem, this tool is GPRbuild. GPRbuild is a multi-language, extensible build system that automates the process of compiling, binding, and linking a set of sources into a library or an executable.

GPRbuild operates using project files, which have a .gpr extension. A project file is a text file with an Ada-like syntax that describes the structure of the project, including the location of source files, the name of the main subprogram, and compiler options.

The GPR Project File

The project file serves as the central configuration for the build process. When a new project is initialized with Alire, a basic .gpr file is generated automatically. A minimal project file for an executable program includes several key attributes declared within a project block.

Consider a project named App with the following structure:

app/
├── app.gpr
└── src/
    └── main.adb

The corresponding app.gpr file would contain the following declarations:

project App is

   for Source_Dirs use ("src");
   for Object_Dir use "obj";
   for Exec_Dir use "bin";

   for Main use ("main.adb");

end App;

The attributes within this project file specify the following:

  • project App is ... end App;: Defines a project named App. The name of the project file (app.gpr) should typically match the project name (App).
  • for Source_Dirs use ("src");: A list of directories where GPRbuild will search for source files. In this case, it is a single directory named src, relative to the location of the .gpr file.
  • for Object_Dir use "obj";: The directory where the compiler will place intermediate object files and the Ada Library Information (.ali) files.
  • for Exec_Dir use "bin";: The directory where GPRbuild will place the final executable file after a successful link.
  • for Main use ("main.adb");: Specifies the name of the source file containing the main subprogram that serves as the entry point for the executable.

Using Alire to Drive GPRbuild

Alire acts as a high-level front-end to GPRbuild, simplifying project management significantly. It automatically handles dependency resolution and constructs the necessary gprbuild command-line invocations.

The primary Alire commands for building and running a project are:

  • alr build: This command invokes GPRbuild to compile and link the project defined by the .gpr file in the current directory. GPRbuild performs an incremental build, meaning it only recompiles files that have been modified since the last build, or files that depend on modified files.
  • alr run: This command first ensures the project is built (by implicitly running alr build if necessary) and then executes the resulting program.

The Build and Clean Cycle

To build the project defined by app.gpr, one would execute the following command from within the app directory:

alr build

This command instructs GPRbuild to:

  1. Parse app.gpr.
  2. Create the obj and bin directories if they do not exist.
  3. Compile src/main.adb into an object file, placing it in the obj directory.
  4. Link the object file with the necessary Ada run-time libraries to create an executable file named main (or main.exe on Windows) inside the bin directory.

To remove all artifacts generated by the build process, the gprclean tool is used. Unlike build and run, there is not a standard Alire command for this, so gprclean is invoked directly. It requires the project file to be specified with the -P switch.

gprclean -P app.gpr

This command will remove the obj and bin directories and all their contents, returning the project to a clean state.

By integrating with GPRbuild, Alire provides a powerful and consistent workflow for managing the entire lifecycle of an Ada project, from initialization and dependency management to compilation and execution.

2.3 A “Hello, World!” Program

The “Hello, World!” program is a traditional first example in a new programming language. It is a simple program that demonstrates the fundamental structure of an executable Ada program.

The Program Source Code

The following is the complete source code for a “Hello, World!” program in Ada. This code should be saved in a file named hello_world.adb. The file name must match the name of the main subprogram.

with Ada.Text_IO;

procedure hello_world is
begin
   Ada.Text_IO.put_line (item => "Hello, World!");
end hello_world;
  • The with Ada.Text_IO; clause makes the Ada.Text_IO package visible to the program. This package provides subprograms for input and output operations, such as writing text to the console.
  • The procedure hello_world is line declares a parameterless subprogram named hello_world. This is the entry point of the program.
  • The begin and end hello_world; keywords delimit the sequence of statements that will be executed when the program runs.
  • The statement Ada.Text_IO.put_line (item => "Hello, World!"); calls the put_line procedure from the Ada.Text_IO package. This procedure writes the given String literal, "Hello, World!", to the standard output and appends a new line. The named parameter association item => is used for clarity, which is a common practice in Ada.

2.3.1 Compiling and Running

This section details the command-line procedures for compiling and executing an Ada program using the GNAT toolchain. The “Hello, World!” example from the preceding section will be used to illustrate these steps.

Compilation with GNAT

The compilation process translates Ada source code into an executable program. The primary tool within the GNAT toolchain for this purpose is gnatmake. This utility automates the entire build process, including parsing, semantic analysis, code generation, and linking. It automatically manages dependencies specified by with clauses, ensuring that all required units are compiled in the correct order.

For the hello_world.adb file, the compilation command is as follows:

gnatmake hello_world.adb

Executing this command directs the GNAT compiler to process hello_world.adb. The compiler will check the program for syntactic and semantic errors. If the compilation is successful, gnatmake will generate an executable file. The name of this file is typically derived from the main subprogram’s name, resulting in hello_world on Unix-like systems and hello_world.exe on Windows.

Running the Executable

After a successful compilation, the resulting executable can be invoked directly from the command line. To execute the hello_world program, a user can enter the following command:

./hello_world

Upon execution, the program will perform the actions defined in its body. For this specific program, the result is the display of a single line of text on the standard output.

Hello, World!

This fundamental workflow of writing an Ada source file, compiling it with gnatmake, and executing the resulting binary forms the basis for most Ada development.

package Ada_Programming.Getting_Started.Basic_Program_Structure is

   -- This package provides a detailed breakdown of the fundamental
   -- components that constitute a typical Ada program, as demonstrated
   -- by the "Hello, World!" example.

end Ada_Programming.Getting_Started.Basic_Program_Structure;

2.3.2 Basic Program Structure

The structure of an Ada program is modular and hierarchical. A simple executable program, such as “Hello, World!”, is composed of three primary structural components. These components facilitate readability and help ensure correctness by explicitly defining dependencies and execution flow.

The Context Clause

The context clause, introduced by the reserved word with, specifies the external program units that the current unit requires. For the “Hello, World!” program, the line with Ada.Text_IO; serves this purpose.

The with clause grants direct visibility to the public interface of the specified package, Ada.Text_IO. This makes the package’s declared entities, such as the put_line procedure, available for use within the program. All external dependencies must be explicitly declared using a with clause at the beginning of the compilation unit.

The Program Unit Declaration

The program’s entry point is defined by a program unit declaration. For standalone executable programs, this is a parameterless procedure declaration. The declaration consists of the reserved word procedure, followed by the identifier that names the procedure, and a semicolon.

procedure hello_world is

The program unit name, hello_world in this case, corresponds to the name of the file (hello_world.adb). The reserved word is signals the beginning of the procedure’s declarative part, which in this minimal example is empty.

The Statement Block

The statement block contains the sequence of statements that are executed when the program runs. It is enclosed by the reserved words begin and end. Each statement within the block is terminated by a semicolon.

begin
   Ada.Text_IO.put_line (item => "Hello, World!");
end hello_world;

This block represents the control flow of the program’s main procedure. In this example, the block contains a single statement: a call to the put_line procedure to display a string. The end clause must be followed by the name of the program unit being completed, hello_world, to improve clarity and assist the compiler in verifying the program’s structure.

Part II: The Building Blocks of Ada

3. Lexical Elements and Syntax

The fundamental components of any Ada program are its lexical elements. These are the smallest units of program text that have a meaning to the compiler, akin to words in a human language. Understanding these elements is essential for writing syntactically correct and readable code.

3.1 The Ada Character Set

[cite_start]The Ada character set is extensive, drawing from the ISO/IEC 10646:2020 Universal Coded Character Set (UCS) standard[cite: 1]. This includes a wide range of characters from various languages, which can be used in identifiers and comments. [cite_start]The character set is organized into several categories, such as uppercase letters, lowercase letters, decimal digits, and special characters[cite: 1].

[cite_start]For the purpose of program text, only characters from certain categories are permitted outside of comments[cite: 1]. [cite_start]The standard mandates that implementations must accept Ada source code in UTF-8 encoding[cite: 1]. This ensures that code written in Ada can be portable across different systems and environments that support this encoding.

3.2 Identifiers and Reserved Words

[cite_start]An identifier is a sequence of characters used as a name for declared entities[cite: 1]. This can include variables, subprograms, types, and more. [cite_start]An identifier starts with a letter and can be followed by any combination of letters, digits, and underscores, with a few restrictions[cite: 1]:

  • An identifier cannot contain two consecutive underscore characters.
  • An identifier cannot end with an underscore.

The language-defined rules for identifiers promote readability. [cite_start]Importantly, Ada identifiers are case-insensitive after applying a case-folding rule[cite: 1]. This means that My_Variable, my_variable, and MY_VARIABLE all refer to the same entity.

[cite_start]A set of reserved words is defined within the language[cite: 1]. These words have a special meaning and cannot be used as identifiers. [cite_start]Examples include package, procedure, function, is, and begin[cite: 1]. The full list of reserved words can be found in the appendix.

3.3 Declarations, Statements, and Expressions

The structure of an Ada program is built upon declarations, statements, and expressions.

  • [cite_start]Declarations introduce new entities to the program and associate them with a name[cite: 1]. [cite_start]This includes everything from a simple variable declaration (Count : Integer;) to complex type and package declarations[cite: 1].
  • [cite_start]Statements define actions to be performed during program execution[cite: 1]. [cite_start]Examples include assignment statements (Value := 10;), control flow statements (if and loop), and subprogram calls (Reset_Counter;)[cite: 1].
  • [cite_start]Expressions are formulas that compute or retrieve a value[cite: 1]. They are composed of primaries (literals, names, function calls) and operators. [cite_start]Examples include 2 + 2, X * Y, and My_Function(A, B)[cite: 1].

[cite_start]A program’s executable code is organized into sequences of statements, typically found within subprogram bodies, task bodies, or protected bodies[cite: 1].

3.4 Pragmas and Aspects

[cite_start]Pragmas are compiler directives that provide instructions or advice to the compiler[cite: 1]. [cite_start]They do not change the meaning of the program but can affect its compilation, optimization, or behavior[cite: 1]. [cite_start]For example, pragma Assert can be used to check the truth of a condition at runtime, while pragma Optimize can advise the compiler to prioritize speed or memory usage[cite: 1]. [cite_start]The names of language-defined pragmas appear in lowercase[cite: 1].

[cite_start]Aspects are specifiable properties of an entity that can be declared using with clauses in the entity’s declaration[cite: 1]. [cite_start]Aspects can be either representation aspects (affecting how an entity is mapped to hardware) or operational aspects (affecting an entity’s behavior)[cite: 1]. [cite_start]Many attributes, such as Size or Alignment, can be specified using an aspect clause[cite: 1].

An aspect is specified using an aspect_specification on a declaration, such as:

type My_Type is new Integer
  with Default_Value => 0;

Here, the Default_Value operational aspect is specified to ensure that any uninitialized object of My_Type will have a value of 0.

4. The Type System

Ada’s strong type system is a cornerstone of its design philosophy, which prioritizes correctness and reliability. A type defines a set of values and a set of operations applicable to those values. Unlike many languages, Ada’s type system is not just a mechanism for memory allocation; it is a tool for expressing and enforcing design intentions.

4.1 Strong Typing

Ada is a strongly typed language, meaning that the compiler enforces strict rules about how different types can interact. This prevents many common programming errors at compile time, such as assigning a String to an Integer or performing an operation on a variable that is not defined for its type. The strong typing system ensures that type mismatches are caught early, reducing the likelihood of runtime failures.

This strictness requires explicit type conversions when an operation involves different, but compatible, types. For example, converting a Long_Integer to an Integer requires a specific conversion syntax. This explicit approach makes the programmer’s intent clear and avoids silent, potentially erroneous, implicit conversions.

4.2 Scalar Types

Scalar types are those whose values have no components. They can be ordered and are often represented by a single machine word.

4.2.1 Integer Types

Ada provides several predefined integer types, such as Integer, Natural, and Positive, but also allows you to define your own integer types with specific ranges. This feature is crucial for modeling real-world data and constraints. A custom integer type can be defined with a specific range, and the compiler will ensure that all values of that type remain within that range.

For example, to represent a temperature sensor’s output, which is known to be within a specific range, you can declare a new type:

type Temperature is range -100 .. 100;

Any attempt to assign a value outside of this range to a variable of type Temperature will result in a compile-time or runtime error.

4.2.2 Enumeration Types

Enumeration types allow you to define a list of discrete, named values. This makes code more readable and self-documenting compared to using magic numbers.

For example, the days of the week can be represented by a simple enumeration:

type Day_Of_Week is (monday, tuesday, wednesday, thursday, friday, saturday, sunday);

Variables of type Day_Of_Week can only hold one of these named values.

4.2.3 Floating-Point and Fixed-Point Types

Ada supports both floating-point and fixed-point types for representing real numbers.

  • Floating-point types (Float, Long_Float) are approximations and are suitable for general-purpose scientific computing where precision is not an absolute requirement.
  • Fixed-point types provide a more precise, programmer-controlled way to handle real numbers, which is essential for many real-time and embedded applications. A fixed-point type is defined by its range and a delta value, which specifies the minimum precision.
type Voltage is delta 0.01 range 0.0 .. 100.0;

This declaration specifies a type where values are represented with a precision of two decimal places, within a range of 0.0 to 100.0.

4.2.4 The Boolean Type

The predefined Boolean type is an enumeration type with two values: False and True. It is fundamental to control flow and conditional expressions.

is_active : Boolean := True;

4.2.5 Character Types

Ada defines three standard character types:

  • Character: Represents characters from the ISO 8859-1 set.
  • Wide_Character: Represents characters from the UCS-2 (Unicode) set.
  • Wide_Wide_Character: Represents characters from the UCS-4 (full Unicode) set, also known as UTF-32.

4.3 Subtypes and Derived Types

Ada provides two key mechanisms for creating new types from existing ones:

  • Derived Types: Creates a new, distinct type that is a copy of a parent type. The new type inherits all the operations of the parent but is considered incompatible for type-safe reasons. This is Ada’s primary mechanism for enforcing strong typing.
  • Subtypes: Defines a new constraint on an existing type. A subtype is not a new type; it is simply a type with a restricted range of values. Subtypes are compatible with their base type, but a check is performed at runtime to ensure the constraint is not violated.

4.4 Attributes of Scalar Types ('image, 'value)

Ada provides a number of attributes that allow you to query information about a type or a variable. Two particularly useful attributes for scalar types are 'image and 'value:

  • 'image returns a String representation of a value.
  • 'value is a function that takes a String and returns the corresponding value of the type.

These attributes are essential for converting between internal values and their external textual representations, especially in I/O operations.

type My_Int is range 1 .. 10;
my_num    : My_Int := 5;
num_text  : constant String := My_Int'image (my_num); -- " 5"
new_num   : My_Int := My_Int'value (" 8");

4.5 User-Defined Type Ranges and Predicates

Building on the concept of subtypes, Ada 2012 introduced type predicates which allow you to specify more complex constraints on a type. A predicate is a boolean expression that must be true for all values of the type.

A static predicate is checked by the compiler, while a dynamic predicate is checked at runtime. Predicates can be specified as part of a type’s declaration using the with aspect.

subtype Positive_Even_Integer is Integer range 1 .. Integer'last
  with Dynamic_Predicate => Positive_Even_Integer mod 2 = 0;

This declares a subtype Positive_Even_Integer where all values must be positive and even. The compiler will check this condition at runtime whenever a value is assigned to a variable of this type. This is a powerful feature for enforcing data integrity at the type level.

5. Expressions and Operators

5.1 Operators and Precedence

5.2 Conditional Expressions (if, case)

5.3 Quantified Expressions (for all, for some)

5.4 Membership Tests (in, not in)

5.5 Type Conversions and Qualifications

6. Control Flow

6.1 Sequential Statements

6.2 Conditional Statements (if, case)

6.3 Loop Statements (loop, while, for)

6.4 Iterators and Iterable Container Types

6.5 Control Transfer (exit, goto, return)

Part III: Structured and Modular Programming

7. Subprograms

7.1 Procedures and Functions

7.2 Parameter Modes (in, out, in out, in out all)

7.3 Overloading and Operator Overloading

7.4 Null Procedures

7.5 Expression Functions

7.6 Pre- and Post-conditions (with pre, with post)

8. Packages and Encapsulation

8.1 Information Hiding

8.2 Package Specifications and Bodies

8.3 Child Packages and Hierarchies

8.4 Pre-elaborable Packages

9. Visibility and Program Structure

9.1 Visibility Rules and Declaration Scopes

9.2 The use and renames Clauses

9.3 Compilation Units and Subunits

9.4 Elaboration Order

10. Exception Handling

10.1 Declaring and Raising Exceptions

10.2 Handling Exceptions

10.3 The others Choice

10.4 Exception Propagation and Information

Part IV: Advanced Data Structures

11. Composite Types

11.1 Array Types

11.1.1 Constrained and Unconstrained Arrays

11.1.2 The String Type

11.1.3 Array Attributes, Slices, and Aggregates

11.2 Record Types

11.2.1 Basic Record Declarations

11.2.2 Discriminants and Variant Records

11.2.3 Per-Object Constraints

11.3 Delta Aggregates

11.4 The 'image Attribute for Composite Types

12. Access Types and Memory Management

12.1 Pointers and Memory in Ada

12.2 Declaring Access Types

12.2.1 Pool-Specific and Anonymous Access Types

12.3 The new Allocator

12.4 Unchecked Deallocation and Storage Pools

12.5 Null Access Values

Part V: Object-Oriented and Generic Programming

13. Object-Oriented Programming

13.1 Tagged Types and Inheritance

13.2 Type Extension and Derivation

13.3 Class-wide Types (T'class)

13.4 Primitive Operations and Dispatching

13.5 Abstract Types and Interfaces

14. Generic Programming

14.1 Generic Units

14.2 Generic Formal Parameters

14.2.1 Objects, Types, and Subprograms

14.3 Instantiation

14.4 Formal Packages

14.5 Formal Container and Iterator Packages

Part VI: Concurrent and Real-Time Programming

15. Tasking and Concurrency

15.1 task type and task object

15.2 Task Activation, Execution, and Termination

15.3 The delay Statement

15.4 Task Scheduling and Priorities

16. Synchronization and Communication

16.1 Protected Objects

16.1.1 Protected Procedures, Functions, and Entries

16.2 The Rendezvous and Guarded Entries

16.3 The select Statement

16.4 Asynchronous Transfer of Control

17. Parallel Programming Constructs

17.1 Parallel for Loops

17.2 Parallel Blocks

17.3 Chunk and Work-Stealing Iterators

18. Advanced Concurrency and Real-Time Features

18.1 The Ravenscar Profile

18.2 CPU and Dispatching Affinity

18.3 Timing Events and Execution-Time Clocks

18.4 Group Budgets and Deadlines

18.5 Earliest Deadline First (EDF) Schedulers

18.6 Synchronous Task and Protected Objects

Part VII: Systems Programming and Interfacing

19. Representation Clauses and Bit Manipulation

19.1 Controlling Data Representation

19.1.1 Record Layouts

19.1.2 Enumeration Representation

19.2 Bitwise Operations

19.3 The System Package and Address Arithmetic

20. Interfacing with Other Languages

20.1 The Interfaces Package

20.2 Interfacing with C and C++

20.3 Interfacing with Fortran and COBOL

20.4 pragma import and pragma export

21. Unchecked Programming

21.1 Unchecked_Conversion

21.2 Unchecked_Deallocation

21.3 Unchecked_Access

21.4 Volatile and Atomic Objects

Part VIII: The Predefined Library

22. Input/Output

22.1 Ada.Text_IO, Ada.Wide_Text_IO, Ada.Wide_Wide_Text_IO

22.2 Ada.Text_IO.Editing for Formatted Output

22.3 Ada.Sequential_IO, Ada.Direct_IO, Ada.Stream_IO

22.4 Ada.Command_Line and Environment Variables

22.5 Ada.IO_Exceptions

23. String and Text Manipulation

23.1 Fixed, Bounded, and Unbounded Strings

23.2 Ada.Strings.Maps and Character Sets

23.3 Ada.Strings.Text_Buffers

23.4 Ada.Strings.UTF_Encoding

23.5 Regular Expressions

24. The Container Library

24.1 Vectors and Doubly Linked Lists

24.2 Maps and Sets (Hashed and Ordered)

24.3 Cursors, Iterators, and Algorithms

24.4 Indefinite and Bounded Containers

25. Numerics and Utilities

25.1 Ada.Numerics for Elementary Functions

25.2 Ada.Numerics.Generic_Complex_Types

25.3 Ada.Numerics.Generic_Real_Arrays

25.4 Ada.Numerics.Big_Numbers (Big_Integers, Big_Reals)

25.5 Ada.Calendar and Ada.Real_Time

25.6 Ada.Directories and Ada.Environment_Variables

25.7 Ada.Task_Attributes

25.8 Ada.Lexical_Analysis

Part IX: High-Integrity and Formal Methods

26. High-Integrity Programming

26.1 The Restrictions and Detect_Blocking Pragmas

26.2 The No_Return and Reviewable Aspects

26.3 Other High-Integrity Features

27. Contract-Based Programming

27.1 Type Invariants

27.2 Subtype Predicates

27.3 Pre-, Post-conditions, and Assertions

27.4 Stable Properties and Functions

27.5 Ghost Code and Verification-Oriented Pragmas

28. Introduction to SPARK

28.1 Formal Verification with SPARK

28.2 The SPARK Language Subset

28.3 Proving Program Properties with GNATprove

28.4 Flow Analysis and Proof

Part X: Conclusion

29. The Future of Ada

29.1 The Continuing Evolution of the Language

29.2 Further Reading and Community Resources

Appendices

Appendix A: Ada Reserved Words and Attributes

Appendix B: Pragmas and Aspects

Appendix C: Implementation-Defined Attributes and Pragmas

Appendix D: Overview of Specialized Annexes

Appendix E: Clair Coding Style Guide

  • Indentation: Use 2 spaces for indentation, not tabs.

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

      • Rationale: To distinguish language keywords and aspects from user-defined identifiers.
      • 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: For consistency with other language keywords and attributes. 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), pragma no_return
  • Spacing:
    • Subprogram Calls & Declarations: Use a single space between the subprogram name and the opening parenthesis (.
      • Rationale: To visually distinguish subprogram names from type conversions or other language constructs that use parentheses, improving overall code clarity.
      • Example (Call): Clair.Error.get_error_message (errno_code);
      • Example (Declaration): procedure exit_process (status : Integer := EXIT_SUCCESS);
  • 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 distinguish language-defined attributes from user-defined types and subprograms.
    • Example: errmsg'length, c_path'address
  • Types, Subtypes, Exceptions & Protected Objects:
    • Use Pascal_Case for single-word identifiers.
      • Rationale: Within a package like File, a name like Descriptor is self-evident when used as File.Descriptor. Adding a redundant prefix, such as in File.File_Descriptor, can harm readability.
      • Example: Descriptor, Flags, Object
    • Use Pascal_Case_With_Underscores for multi-word identifiers.
      • Rationale: To clearly distinguish the words within a multi-word type name, improving readability.
      • Example: Library_Load_Error, Symbol_Lookup_Error
  • 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