Ada Programming
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.
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
Table of Contents
- Preface
- Part I: Foundations of Ada
- 1. The Ada Paradigm
- 2. Your First Ada Program
- Part II: The Building Blocks of Ada
- 3. Lexical Elements and Syntax
- 4. The Type System
- 5. Expressions and Operators
- 6. Control Flow
- Part III: Structured and Modular Programming
- 7. Subprograms
- 8. Packages and Encapsulation
- 9. Visibility and Program Structure
- 10. Exception Handling
- Part IV: Advanced Data Structures
- 11. Composite Types
- 12. Access Types and Memory Management
- Part V: Object-Oriented and Generic Programming
- 13. Object-Oriented Programming
- 14. Generic Programming
- Part VI: Concurrent and Real-Time Programming
- 15. Tasking and Concurrency
- 16. Synchronization and Communication
- 17. Parallel Programming Constructs
- 18. Advanced Concurrency and Real-Time Features
- Part VII: Systems Programming and Interfacing
- 19. Representation Clauses and Bit Manipulation
- 20. Interfacing with Other Languages
- 21. Unchecked Programming
- Part VIII: The Predefined Library
- 22. Input/Output
- 23. String and Text Manipulation
- 24. The Container Library
- 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
andAda.Real_Time
- 25.6
Ada.Directories
andAda.Environment_Variables
- 25.7
Ada.Task_Attributes
- 25.8
Ada.Lexical_Analysis
- 25.1
- Part IX: High-Integrity and Formal Methods
- 26. High-Integrity Programming
- 27. Contract-Based Programming
- 28. Introduction to SPARK
- Part X: Conclusion
- 29. The Future of Ada
- Appendices
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 andparallel 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
andGlobal'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 newPut_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 namedApp
. 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 namedsrc
, 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 runningalr 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:
- Parse
app.gpr
. - Create the
obj
andbin
directories if they do not exist. - Compile
src/main.adb
into an object file, placing it in theobj
directory. - Link the object file with the necessary Ada run-time libraries to create an executable file named
main
(ormain.exe
on Windows) inside thebin
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 theAda.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 namedhello_world
. This is the entry point of the program. - The
begin
andend 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 theput_line
procedure from theAda.Text_IO
package. This procedure writes the givenString
literal,"Hello, World!"
, to the standard output and appends a new line. The named parameter associationitem =>
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
andloop
), 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
, andMy_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 asUTF-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 aString
representation of a value.'value
is a function that takes aString
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
- Rationale: For consistency with other language keywords and attributes. This applies to both the pragma itself (e.g.,
- 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);
- Subprogram Calls & Declarations: Use a single space between the subprogram name and the opening parenthesis
-
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);
- Rationale: This is a widely understood convention that avoids potential conflicts with the
- 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 likeDescriptor
is self-evident when used asFile.Descriptor
. Adding a redundant prefix, such as inFile.File_Descriptor
, can harm readability. - Example:
Descriptor
,Flags
,Object
- Rationale: Within a package like
- 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
- Use
- 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;
- Compile-Time Constants: Use
- 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
, notClair.Dl
which can look likeClair.D1
).
- Example:
- Standard Library Naming:
Interfaces.C
: Types and subprograms from theInterfaces.C
package and its children should usesnake_case
to match the C standard library’s naming convention. Constants from this package follow the globalUPPER_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